001: /*
002: * $Id: JGraphpadSVGServer.java,v 1.2 2006/01/31 12:10:34 gaudenz Exp $
003: * Copyright (c) 2001-2005, Gaudenz Alder
004: *
005: * All rights reserved.
006: *
007: * See LICENSE file for license details. If you are unable to locate
008: * this file please contact info (at) jgraph (dot) com.
009: */
010: package com.jgraph.svgplugin;
011:
012: import java.awt.Color;
013: import java.awt.image.BufferedImage;
014: import java.io.BufferedReader;
015: import java.io.ByteArrayInputStream;
016: import java.io.ByteArrayOutputStream;
017: import java.io.IOException;
018: import java.io.InputStream;
019: import java.io.InputStreamReader;
020: import java.io.OutputStream;
021: import java.io.PrintWriter;
022: import java.net.ServerSocket;
023: import java.net.Socket;
024: import java.net.URLEncoder;
025: import java.util.Date;
026: import java.util.Enumeration;
027: import java.util.Locale;
028: import java.util.Properties;
029: import java.util.StringTokenizer;
030: import java.util.TimeZone;
031:
032: import javax.imageio.ImageIO;
033: import javax.swing.JPanel;
034:
035: import org.jgraph.JGraph;
036: import org.jgraph.graph.GraphLayoutCache;
037:
038: import com.jgraph.JGraphEditor;
039: import com.jgraph.JGraphpad;
040: import com.jgraph.editor.JGraphEditorDiagram;
041: import com.jgraph.editor.JGraphEditorModel;
042: import com.jgraph.editor.JGraphEditorResources;
043: import com.jgraph.pad.util.JGraphpadImageEncoder;
044:
045: /**
046: * Simple webserver to stream SVG, PNG and JPG content to clients. This
047: * implementation is based on nanoHttpd (http://nanohttpd.sourceforge.net/).
048: */
049: public class JGraphpadSVGServer {
050:
051: /**
052: * Some HTTP response status codes
053: */
054: public static final String HTTP_OK = "200 OK",
055: HTTP_REDIRECT = "301 Moved Permanently",
056: HTTP_FORBIDDEN = "403 Forbidden",
057: HTTP_NOTFOUND = "404 Not Found",
058: HTTP_BADREQUEST = "400 Bad Request",
059: HTTP_INTERNALERROR = "500 Internal Server Error",
060: HTTP_NOTIMPLEMENTED = "501 Not Implemented";
061:
062: /**
063: * Common mime types for dynamic content
064: */
065: public static final String MIME_PLAINTEXT = "text/plain",
066: MIME_HTML = "text/html",
067: MIME_DEFAULT_BINARY = "application/octet-stream",
068: MIME_PNG = "image/png", MIME_JPG = "image/jpeg";
069:
070: /**
071: * Holds the socket the server is listening on.
072: */
073: protected ServerSocket serverSocket;
074:
075: /**
076: * References the enclosing editor.
077: */
078: protected JGraphEditor editor;
079:
080: /**
081: * Starts a HTTP server for the enclosing editor on the specified port.
082: * <p>
083: * Throws an IOException if the socket is already in use
084: */
085: public JGraphpadSVGServer(JGraphEditor editor, int port)
086: throws IOException {
087: this .editor = editor;
088: serverSocket = new ServerSocket(port);
089: Thread t = new Thread(new Runnable() {
090: public void run() {
091: try {
092: while (true)
093: new HTTPSession(serverSocket.accept());
094: } catch (IOException ioe) {
095: // ignore
096: }
097: }
098: });
099: t.setDaemon(true);
100: t.start();
101: }
102:
103: /**
104: * Serves the response for the specified request. This returns a HTML index
105: * containing the links to the diagrams in the editor's document model, or
106: * one of the diagrams as an SVG, JPG or PNG image.
107: *
108: * @param uri
109: * Percent-decoded URI without parameters, for example
110: * "/index.cgi"
111: * @param method
112: * "GET", "POST" etc.
113: * @param parms
114: * Parsed, percent decoded parameters from URI and, in case of
115: * POST, data.
116: * @param header
117: * Header entries, percent decoded
118: * @return HTTP response, see class Response for details
119: * @throws IOException
120: */
121: public Response serve(String uri, String method, Properties header,
122: Properties parms) throws IOException {
123: Response response = null;
124: String format = parms.getProperty("format");
125: JGraph graph = getGraph(uri);
126: if (uri.equals("/"))
127: response = serveIndex();
128: else if (graph != null) {
129:
130: // Responds with a simple HTML info if the graph is empty
131: if (graph.getModel().getRootCount() == 0)
132: response = new Response(HTTP_OK, MIME_HTML,
133: JGraphEditorResources
134: .getString("GraphContainsNoData"));
135:
136: // Otherwise responds with the image in the requested format
137: else if (format == null)
138: response = serveSVG(graph);
139: else if (JGraphpad.isImage(format))
140: response = serveImage(graph, format);
141: }
142: return response;
143: }
144:
145: /**
146: * Produces an SVG image of the specified graph.
147: */
148: protected Response serveSVG(JGraph graph) throws IOException {
149: OutputStream out = new ByteArrayOutputStream();
150: JGraphpadSVGAction.writeSVG(graph, out, 10);
151: out.close();
152: return new Response(HTTP_OK, JGraphpadSVGAction.MIME_SVG, out
153: .toString());
154: }
155:
156: /**
157: * Produces a JPG or PNG image of the specified graph.
158: */
159: protected Response serveImage(JGraph graph, String format)
160: throws IOException {
161: ByteArrayOutputStream out = new ByteArrayOutputStream();
162: BufferedImage img = graph.getImage(Color.white, 5);
163: if (format.equalsIgnoreCase("gif"))
164: JGraphpadImageEncoder.writeGIF(img, out);
165: else
166: ImageIO.write(img, format, out);
167: out.close();
168: return new Response(HTTP_OK,
169: (format.equalsIgnoreCase("png")) ? MIME_PNG : MIME_JPG,
170: out.toByteArray());
171: }
172:
173: /**
174: * Produces a HTML index page of all diagrams in the document model.
175: */
176: protected Response serveIndex() {
177: String content = "";
178: JGraphEditorModel model = (JGraphEditorModel) editor.getModel();
179: Object root = model.getRoot();
180: int childCount = model.getChildCount(root);
181: if (childCount == 0)
182: content += JGraphEditorResources.getString("NoDocument");
183: for (int i = 0; i < model.getChildCount(root); i++) {
184: Object child = model.getChild(root, i);
185: for (int j = 0; j < model.getChildCount(child); j++) {
186: Object diagram = model.getChild(child, j);
187: String label = String.valueOf(child) + "."
188: + String.valueOf(diagram);
189: content += label + ": ";
190: content += "<a href=\"" + String.valueOf(i) + "/"
191: + String.valueOf(j) + "/\">SVG</a> ";
192: content += "<a href=\"" + String.valueOf(i) + "/"
193: + String.valueOf(j)
194: + "?format=png\">PNG</a> ";
195: content += "<a href=\"" + String.valueOf(i) + "/"
196: + String.valueOf(j)
197: + "?format=jpg\">JPG</a> ";
198: content += "<a href=\"" + String.valueOf(i) + "/"
199: + String.valueOf(j)
200: + "?format=gif\">GIF</a></br>";
201: }
202: }
203: return new Response(HTTP_OK, MIME_HTML, content);
204: }
205:
206: /**
207: * Returns a JGraph for the specified reference. The refence is of the form
208: * i/j/k/... where i is the index of the first parent in the model, j is the
209: * index of the child etc. If the model element is a
210: * {@link JGraphEditorDiagram} then a graph is created using
211: * {@link #createGraph(GraphLayoutCache)} and returned.
212: */
213: protected JGraph getGraph(String reference) {
214: try {
215: Object parent = editor.getModel().getRoot();
216: String[] path = reference.substring(1).split("/");
217: for (int i = 0; i < path.length; i++)
218: parent = editor.getModel().getChild(parent,
219: Integer.parseInt(path[i]));
220: if (parent instanceof JGraphEditorDiagram) {
221: GraphLayoutCache cache = ((JGraphEditorDiagram) parent)
222: .getGraphLayoutCache();
223: return createGraph(cache);
224: }
225: } catch (Exception e) {
226: // ignore
227: }
228: return null;
229: }
230:
231: /**
232: * Creates a new graph for the specified cache and puts it into the
233: * backingFrame component hierachy.
234: */
235: protected JGraph createGraph(GraphLayoutCache cache) {
236: JGraph graph = editor.getFactory().createGraph(cache);
237: // "Headless Swing Hack"
238: JPanel panel = new JPanel();
239: panel.setDoubleBuffered(false);
240: panel.add(graph);
241: panel.setVisible(true); // required
242: panel.setEnabled(true); // also required
243: panel.addNotify(); // simlutes pack on jframe
244: panel.validate();
245: return graph;
246: }
247:
248: /**
249: * HTTP response. Return one of these from serve().
250: */
251: public class Response {
252: /**
253: * Default constructor: response = HTTP_OK, data = mime = 'null'
254: */
255: public Response() {
256: this .status = HTTP_OK;
257: }
258:
259: /**
260: * Basic constructor.
261: */
262: public Response(String status, String mimeType, InputStream data) {
263: this .status = status;
264: this .mimeType = mimeType;
265: this .data = data;
266: }
267:
268: /**
269: * Convenience method that makes an InputStream out of given text.
270: */
271: public Response(String status, String mimeType, String txt) {
272: this (status, mimeType, txt.getBytes());
273: }
274:
275: /**
276: * Convenience method that makes an InputStream out of given text.
277: */
278: public Response(String status, String mimeType, byte[] bytes) {
279: this .status = status;
280: this .mimeType = mimeType;
281: this .data = new ByteArrayInputStream(bytes);
282: }
283:
284: /**
285: * Convenience method that makes an InputStream out of given text.
286: */
287: public Response(String status, String mimeType, OutputStream out) {
288: this .status = status;
289: this .mimeType = mimeType;
290: }
291:
292: /**
293: * Adds given line to the header.
294: */
295: public void addHeader(String name, String value) {
296: header.put(name, value);
297: }
298:
299: /**
300: * HTTP status code after processing, eg "200 OK", HTTP_OK
301: */
302: public String status;
303:
304: /**
305: * MIME type of content, e.g. "text/html"
306: */
307: public String mimeType;
308:
309: /**
310: * Data of the response, may be null.
311: */
312: public InputStream data;
313:
314: /**
315: * Headers for the HTTP response. Use addHeader() to add lines.
316: */
317: public Properties header = new Properties();
318: }
319:
320: /**
321: * Handles one session, i.e. parses the HTTP request and returns the
322: * response.
323: */
324: private class HTTPSession implements Runnable {
325: public HTTPSession(Socket s) {
326: mySocket = s;
327: Thread t = new Thread(this );
328: t.setDaemon(true);
329: t.start();
330: }
331:
332: public void run() {
333: try {
334: InputStream is = mySocket.getInputStream();
335: if (is == null)
336: return;
337: BufferedReader in = new BufferedReader(
338: new InputStreamReader(is));
339:
340: // Read the request line
341: StringTokenizer st = new StringTokenizer(in.readLine());
342: if (!st.hasMoreTokens())
343: sendError(HTTP_BADREQUEST,
344: "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
345:
346: String method = st.nextToken();
347:
348: if (!st.hasMoreTokens())
349: sendError(HTTP_BADREQUEST,
350: "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
351:
352: String uri = decodePercent(st.nextToken());
353:
354: // Decode parameters from the URI
355: Properties parms = new Properties();
356: int qmi = uri.indexOf('?');
357: if (qmi >= 0) {
358: decodeParms(uri.substring(qmi + 1), parms);
359: uri = decodePercent(uri.substring(0, qmi));
360: }
361:
362: // If there's another token, it's protocol version,
363: // followed by HTTP headers. Ignore version but parse headers.
364: Properties header = new Properties();
365: if (st.hasMoreTokens()) {
366: String line = in.readLine();
367: while (line.trim().length() > 0) {
368: int p = line.indexOf(':');
369: header.put(line.substring(0, p).trim(), line
370: .substring(p + 1).trim());
371: line = in.readLine();
372: }
373: }
374:
375: // If the method is POST, there may be parameters
376: // in data section, too, read another line:
377: if (method.equalsIgnoreCase("POST"))
378: decodeParms(in.readLine(), parms);
379:
380: // Ok, now do the serve()
381: Response r = serve(uri, method, header, parms);
382: if (r == null)
383: sendError(HTTP_INTERNALERROR,
384: "SERVER INTERNAL ERROR: Serve() returned a null response.");
385: else
386: sendResponse(r.status, r.mimeType, r.header, r.data);
387:
388: in.close();
389: } catch (Exception ioe) {
390: try {
391: sendError(HTTP_INTERNALERROR,
392: "SERVER INTERNAL ERROR: IOException: "
393: + ioe.getMessage());
394: } catch (Throwable t) {
395: }
396: }
397: }
398:
399: /**
400: * Decodes the percent encoding scheme. <br/>For example:
401: * "an+example%20string" -> "an example string"
402: */
403: private String decodePercent(String str)
404: throws InterruptedException {
405: try {
406: StringBuffer sb = new StringBuffer();
407: for (int i = 0; i < str.length(); i++) {
408: char c = str.charAt(i);
409: switch (c) {
410: case '+':
411: sb.append(' ');
412: break;
413: case '%':
414: sb.append((char) Integer.parseInt(str
415: .substring(i + 1, i + 3), 16));
416: i += 2;
417: break;
418: default:
419: sb.append(c);
420: break;
421: }
422: }
423: return new String(sb.toString().getBytes());
424: } catch (Exception e) {
425: sendError(HTTP_BADREQUEST,
426: "BAD REQUEST: Bad percent-encoding.");
427: return null;
428: }
429: }
430:
431: /**
432: * Decodes parameters in percent-encoded URI-format ( e.g.
433: * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
434: * Properties.
435: */
436: private void decodeParms(String parms, Properties p)
437: throws InterruptedException {
438: if (parms == null)
439: return;
440:
441: StringTokenizer st = new StringTokenizer(parms, "&");
442: while (st.hasMoreTokens()) {
443: String e = st.nextToken();
444: int sep = e.indexOf('=');
445: if (sep >= 0)
446: p.put(decodePercent(e.substring(0, sep)).trim(),
447: decodePercent(e.substring(sep + 1)));
448: }
449: }
450:
451: /**
452: * Returns an error message as a HTTP response and throws
453: * InterruptedException to stop furhter request processing.
454: */
455: private void sendError(String status, String msg)
456: throws InterruptedException {
457: sendResponse(status, MIME_PLAINTEXT, null,
458: new ByteArrayInputStream(msg.getBytes()));
459: throw new InterruptedException();
460: }
461:
462: /**
463: * Sends given response to the socket.
464: */
465: private void sendResponse(String status, String mime,
466: Properties header, InputStream data) {
467: try {
468: if (status == null)
469: throw new Error(
470: "sendResponse(): Status can't be null.");
471:
472: OutputStream out = mySocket.getOutputStream();
473: PrintWriter pw = new PrintWriter(out);
474: pw.print("HTTP/1.0 " + status + " \r\n");
475:
476: if (mime != null)
477: pw.print("Content-Type: " + mime + "\r\n");
478:
479: if (header == null
480: || header.getProperty("Date") == null)
481: pw.print("Date: " + gmtFrmt.format(new Date())
482: + "\r\n");
483:
484: if (header != null) {
485: Enumeration e = header.keys();
486: while (e.hasMoreElements()) {
487: String key = (String) e.nextElement();
488: String value = header.getProperty(key);
489: pw.print(key + ": " + value + "\r\n");
490: }
491: }
492:
493: pw.print("\r\n");
494: pw.flush();
495:
496: if (data != null) {
497: byte[] buff = new byte[2048];
498: int read = 2048;
499: while (read == 2048) {
500: read = data.read(buff, 0, 2048);
501: out.write(buff, 0, read);
502: }
503: }
504: out.flush();
505: out.close();
506: if (data != null)
507: data.close();
508: } catch (IOException ioe) {
509: // Couldn't write? No can do.
510: try {
511: mySocket.close();
512: } catch (Throwable t) {
513: }
514: }
515: }
516:
517: private Socket mySocket;
518:
519: private BufferedReader myIn;
520: };
521:
522: /**
523: * URL-encodes everything between "/"-characters. Encodes spaces as '%20'
524: * instead of '+'.
525: */
526: private String encodeUri(String uri) {
527: String newUri = "";
528: StringTokenizer st = new StringTokenizer(uri, "/ ", true);
529: while (st.hasMoreTokens()) {
530: String tok = st.nextToken();
531: if (tok.equals("/"))
532: newUri += "/";
533: else if (tok.equals(" "))
534: newUri += "%20";
535: else
536: newUri += URLEncoder.encode(tok);
537: }
538: return newUri;
539: }
540:
541: /**
542: * GMT date formatter
543: */
544: private static java.text.SimpleDateFormat gmtFrmt;
545: static {
546: gmtFrmt = new java.text.SimpleDateFormat(
547: "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
548: gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
549: }
550:
551: /**
552: * @return Returns the serverSocket.
553: */
554: public ServerSocket getServerSocket() {
555: return serverSocket;
556: }
557:
558: /**
559: * @param serverSocket
560: * The serverSocket to set.
561: */
562: public void setServerSocket(ServerSocket serverSocket) {
563: this.serverSocket = serverSocket;
564: }
565:
566: }
|