--- /dev/null
+// JpegComFrame.java\r
+// $Id: JpegComFrame.java,v 1.2 2010/06/15 17:52:53 smhuang Exp $\r
+// (c) COPYRIGHT MIT, INRIA and Keio, 1999.\r
+// Please first read the full copyright statement in file COPYRIGHT.html\r
+\r
+package org.w3c.jigsaw.frames;\r
+\r
+import java.io.File;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.net.URLEncoder;\r
+import org.w3c.www.mime.MimeType;\r
+import org.w3c.www.mime.MimeTypeFormatException;\r
+import org.w3c.tools.resources.Attribute;\r
+import org.w3c.tools.resources.AttributeRegistry;\r
+import org.w3c.tools.resources.ProtocolException;\r
+import org.w3c.tools.resources.ResourceException;\r
+import org.w3c.tools.resources.event.AttributeChangedEvent;\r
+import org.w3c.www.http.HTTP;\r
+import org.w3c.www.http.HttpAccept;\r
+import org.w3c.www.http.HttpEntityTag;\r
+import org.w3c.www.http.HttpFactory;\r
+import org.w3c.www.http.HttpInvalidValueException;\r
+import org.w3c.jigsaw.http.Client;\r
+import org.w3c.jigsaw.http.ClientException;\r
+import org.w3c.jigsaw.http.HTTPException;\r
+import org.w3c.jigsaw.http.Reply;\r
+import org.w3c.jigsaw.http.Request;\r
+\r
+import org.w3c.tools.jpeg.JpegHeaders;\r
+import org.w3c.jigsaw.resources.ImageFileResource;\r
+\r
+/**\r
+ * This class will read the comments from a jpeg file and return it\r
+ * depending on the Accept: header\r
+ */\r
+\r
+public class JpegComFrame extends HTTPFrame {\r
+ public static final boolean debug = false;\r
+ /**\r
+ * Attribute index - The comment content type\r
+ */\r
+ protected static int ATTR_COM_TYPE = -1 ;\r
+\r
+ static {\r
+ Attribute a = null ;\r
+ Class cls = null ;\r
+ try {\r
+ cls = Class.forName("org.w3c.jigsaw.frames.JpegComFrame") ;\r
+ //Added by Jeff Huang\r
+ //TODO: FIXIT\r
+ } catch (Exception ex) {\r
+ ex.printStackTrace() ;\r
+ System.exit(1) ;\r
+ }\r
+ // The comment content type\r
+ a = new MimeTypeAttribute("comment-type",\r
+ null,\r
+ Attribute.EDITABLE) ;\r
+ ATTR_COM_TYPE = AttributeRegistry.registerAttribute(cls, a) ;\r
+ }\r
+\r
+ /**\r
+ * the static String of the Vary ehader to be added\r
+ */\r
+ protected static String[] vary = { "Accept" };\r
+\r
+ /**\r
+ * get the content type of the comment embedded in the picture\r
+ * @return a MimeType, or null if undefined\r
+ */\r
+ public MimeType getCommentType() {\r
+ return (MimeType)getValue(ATTR_COM_TYPE, null);\r
+ }\r
+\r
+ /**\r
+ * The comment entity tag\r
+ */\r
+ protected HttpEntityTag cometag = null;\r
+\r
+ /**\r
+ * The comment.\r
+ */\r
+ protected String comment = null;\r
+\r
+ /**\r
+ * Extract the comment from the jpeg image.\r
+ * @return the comment\r
+ */\r
+ protected String getMetadata() {\r
+ if (fresource == null)\r
+ return null;\r
+ File file = fresource.getFile();\r
+ if (file.exists()) {\r
+ String comments[] = null;\r
+ try {\r
+ JpegHeaders headers = new JpegHeaders(file);\r
+ comments = headers.getComments();\r
+ } catch (Exception ex) {\r
+ ex.printStackTrace();\r
+ return "unable to get comment: "+ex.getMessage();\r
+ }\r
+ comment = "";\r
+ for (int i = 0 ; i < comments.length ; i++)\r
+ comment += comments[i];\r
+ if (comment.equals(""))\r
+ comment = "no comment";\r
+ }\r
+ return comment;\r
+ }\r
+\r
+ /**\r
+ * Get the comment Etag\r
+ * @return an instance of HttpEntityTag, or <strong>null</strong> if not\r
+ * defined.\r
+ */\r
+\r
+ public HttpEntityTag getComETag() {\r
+ if (cometag == null) {\r
+ String etag_s = null;\r
+ if (fresource != null) {\r
+ long lstamp = fresource.getFileStamp()+1;\r
+ if ( lstamp >= 0L ) {\r
+ String soid = Integer.toString(getOid(), 32);\r
+ String stamp = Long.toString(lstamp, 32);\r
+ etag_s = Integer.toString(getOid(), 32)+":"\r
+ + Long.toString(lstamp, 32);\r
+ }\r
+ }\r
+ cometag = HttpFactory.makeETag(false, etag_s);\r
+ }\r
+ return cometag;\r
+ }\r
+\r
+ /**\r
+ * Update the cached headers value.\r
+ * Each resource maintains a set of cached values for headers, this\r
+ * allows for a nice sped-up in headers marshalling, which - as the \r
+ * complexity of the protocol increases - becomes a bottleneck.\r
+ */\r
+\r
+ protected void updateCachedHeaders() {\r
+ super.updateCachedHeaders();\r
+ if (comment == null) {\r
+ comment = getMetadata();\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Listen its resource.\r
+ */\r
+ public void attributeChanged(AttributeChangedEvent evt) {\r
+ super.attributeChanged(evt);\r
+ String name = evt.getAttribute().getName();\r
+ if ((name.equals("file-stamp")) || (name.equals("file-stamp")))\r
+ comment = null;\r
+ }\r
+\r
+ public Reply createCommentReply(Request request, int status) {\r
+ Reply reply = request.makeReply(status);\r
+ updateCachedHeaders();\r
+ reply.setContent(comment);\r
+ reply.setContentType(getCommentType());\r
+ reply.setVary(vary);\r
+ if ( lastmodified != null )\r
+ reply.setHeaderValue(Reply.H_LAST_MODIFIED, lastmodified);\r
+ if ( contentencoding != null )\r
+ reply.setHeaderValue(Reply.H_CONTENT_ENCODING,contentencoding);\r
+ if ( contentlanguage != null )\r
+ reply.setHeaderValue(Reply.H_CONTENT_LANGUAGE,contentlanguage);\r
+ long maxage = getMaxAge();\r
+ if ( maxage >= 0 ) {\r
+ if (reply.getMajorVersion() >= 1 ) {\r
+ if (reply.getMinorVersion() >= 1) {\r
+ reply.setMaxAge((int) (maxage / 1000));\r
+ } \r
+ // If max-age is zero, say what you mean:\r
+ long expires = (System.currentTimeMillis()\r
+ + ((maxage == 0) ? -1000 : maxage));\r
+ reply.setExpires(expires);\r
+ }\r
+ }\r
+ // Set the date of the reply (round it to secs):\r
+ reply.setDate((System.currentTimeMillis() / 1000L) * 1000L);\r
+ reply.setETag(getComETag());\r
+ String commenttype = getCommentType().toString();\r
+ reply.setContentLocation(getURL(request).toExternalForm() \r
+ + ";" + URLEncoder.encode(commenttype));\r
+ return reply;\r
+ }\r
+\r
+ public Reply createCommentReply(Request request) {\r
+ return createCommentReply(request, HTTP.OK);\r
+ }\r
+\r
+ /**\r
+ * Check the <code>If-Match</code> condition of that request.\r
+ * @param request The request to check.\r
+ * @return An integer, either <code>COND_FAILED</cond> if condition\r
+ * was checked, but failed, <code>COND_OK</code> if condition was checked\r
+ * and succeeded, or <strong>0</strong> if the condition was not checked\r
+ * at all (eg because the resource or the request didn't support it).\r
+ */\r
+\r
+ public int checkIfMatch(Request request, HttpEntityTag etag) {\r
+ if (fresource != null) {\r
+ HttpEntityTag tags[] = request.getIfMatch();\r
+ if ( tags != null ) {\r
+ // Good, real validators in use:\r
+ if ( etag != null ) {\r
+ // Note: if etag is null this means that the resource has \r
+ // changed and has not been even emited since then...\r
+ for (int i = 0 ; i < tags.length ; i++) {\r
+ HttpEntityTag t = tags[i];\r
+ if (t.getTag().equals(etag.getTag())) {\r
+ if (t.isWeak() || etag.isWeak()) {\r
+ return COND_WEAK;\r
+ } else {\r
+ return COND_OK;\r
+ }\r
+ }\r
+ }\r
+ }\r
+ return COND_FAILED;\r
+ }\r
+ }\r
+ return 0;\r
+ }\r
+\r
+ /**\r
+ * Check the <code>If-None-Match</code> condition of that request.\r
+ * @param request The request to check.\r
+ * @return An integer, either <code>COND_FAILED</cond> if condition\r
+ * was checked, but failed, <code>COND_OK</code> if condition was checked\r
+ * and succeeded, or <strong>0</strong> if the condition was not checked\r
+ * at all (eg because the resource or the request didn't support it).\r
+ */\r
+\r
+ public int checkIfNoneMatch(Request request, HttpEntityTag etag) {\r
+ if (fresource != null) {\r
+ // Check for an If-None-Match conditional:\r
+ HttpEntityTag tags[] = request.getIfNoneMatch();\r
+ if ( tags != null ) {\r
+ if ( etag == null ) {\r
+ return COND_OK;\r
+ }\r
+ int status = COND_OK;\r
+ for (int i = 0 ; i < tags.length ; i++) {\r
+ HttpEntityTag t = tags[i];\r
+ if (t.getTag().equals(etag.getTag())) {\r
+ if (t.isWeak() || etag.isWeak()) {\r
+ status = COND_WEAK;\r
+ } else {\r
+ return COND_FAILED;\r
+ }\r
+ }\r
+ if (t.getTag().equals("*")) {\r
+ if (fresource != null) {\r
+ File f = fresource.getFile();\r
+ if (f.exists()) {\r
+ return COND_FAILED;\r
+ }\r
+ } else {\r
+ return COND_FAILED;\r
+ }\r
+ }\r
+ }\r
+ return status;\r
+ }\r
+ }\r
+ return 0;\r
+ }\r
+\r
+ /**\r
+ * check the validators namely LMT/Etags according to rfc2616 rules\r
+ * @return An integer, either <code>COND_FAILED</cond> if condition\r
+ * was checked, but failed, <code>COND_OK</code> if condition was checked\r
+ * and succeeded, or <strong>0</strong> if the condition was not checked\r
+ * at all (eg because the resource or the request didn't support it).\r
+ */\r
+ public int checkValidators(Request request, HttpEntityTag etag) {\r
+ int v_inm = checkIfNoneMatch(request, etag);\r
+ int v_ims = checkIfModifiedSince(request);\r
+\r
+ if ((v_inm == COND_OK) || (v_ims == COND_OK)) {\r
+ return COND_OK;\r
+ }\r
+ if ((v_inm == COND_FAILED) || (v_ims == COND_FAILED)) {\r
+ return COND_FAILED;\r
+ }\r
+ if ((v_inm == COND_WEAK) || (v_ims == COND_WEAK)) {\r
+ return COND_OK;\r
+ }\r
+ return 0;\r
+ }\r
+\r
+ /**\r
+ * Negotiate.\r
+ * @param request the incomming request.\r
+ * @return true if the client wants the comment, false if the client \r
+ * wants the image.\r
+ */\r
+ protected boolean negotiate(Request request)\r
+ throws ProtocolException\r
+ {\r
+ if ( ! request.hasAccept() ) {\r
+ //return the image\r
+ return false;\r
+ } else {\r
+ // The browser has given some preferences:\r
+ HttpAccept accepts[] = request.getAccept() ;\r
+ \r
+ //two content types image/jpeg and comment-type\r
+ HttpAccept imgAccept = \r
+ getMatchingAccept(accepts, getContentType());\r
+ HttpAccept comAccept = \r
+ getMatchingAccept(accepts, getCommentType());\r
+ \r
+ if ((imgAccept != null) && (comAccept != null)) {\r
+ // go for best MIME match first\r
+ int matchImg = getContentType().match(imgAccept.getMimeType());\r
+ int matchCom = getCommentType().match(comAccept.getMimeType());\r
+\r
+ if (matchImg == matchCom) {\r
+ // equals, use quality\r
+ return (imgAccept.getQuality() < comAccept.getQuality());\r
+ } else {\r
+ return (matchImg < matchCom);\r
+ }\r
+ } else if (comAccept != null)\r
+ return true;\r
+ else\r
+ return false;\r
+ }\r
+ }\r
+\r
+ protected HttpAccept getMatchingAccept(HttpAccept accepts[],\r
+ MimeType mime) \r
+ {\r
+ int jmatch = -1 ;\r
+ int jidx = -1 ;\r
+ for (int i = 0 ; i < accepts.length ; i++) {\r
+ try {\r
+ int match = mime.match(accepts[i].getMimeType());\r
+ if ( match > jmatch ) {\r
+ jmatch = match ;\r
+ jidx = i ;\r
+ }\r
+ } catch (HttpInvalidValueException ivex) {\r
+ // There is a bad acept header here\r
+ // let's be cool and ignore it\r
+ // FIXME we should answer with a Bad Request\r
+ }\r
+ }\r
+ if (jidx < 0)\r
+ return null;\r
+ return accepts[jidx];\r
+ }\r
+\r
+ /**\r
+ * Perform a HEAD request for the associated FileResource.\r
+ * @param request the incomming request.\r
+ * @return A Reply instance\r
+ * @exception ProtocolException If processsing the request failed.\r
+ * @exception ResourceException If the resource got a fatal error.\r
+ */\r
+ protected Reply headFileResource(Request request) \r
+ throws ProtocolException, ResourceException\r
+ {\r
+ if (fresource == null) \r
+ throw new ResourceException("this frame is not attached to a "+\r
+ "FileResource. ("+\r
+ resource.getIdentifier()+")");\r
+ Reply reply = null;\r
+ fresource.checkContent();\r
+ updateCachedHeaders();\r
+ // hack, if ;text/html is there,\r
+ // it will be added at first place of the accept \r
+ String param = null;\r
+ String sfile = request.getURL().getFile();\r
+ int pos = sfile.indexOf(';');\r
+ if (pos != -1) {\r
+ param = (String) request.getState("type");\r
+ }\r
+ if (param != null) {\r
+ HttpAccept acc[] = request.getAccept();\r
+ HttpAccept newacc[] = null;\r
+ if (acc != null) {\r
+ newacc = new HttpAccept[acc.length+1];\r
+ System.arraycopy(acc, 0, newacc, 1, acc.length);\r
+ } else {\r
+ newacc = new HttpAccept[1];\r
+ }\r
+ try {\r
+ newacc[0] = HttpFactory.makeAccept(new MimeType(param), 1.1);\r
+ request.setAccept(newacc);\r
+ } catch (MimeTypeFormatException ex) {\r
+ // not a valid mime type... maybe something else, do not care\r
+ }\r
+ }\r
+ boolean commentOnly = negotiate(request);\r
+ HttpEntityTag etag = null;\r
+ if (commentOnly)\r
+ etag = getComETag();\r
+ else\r
+ etag = getETag();\r
+ // Check validators:\r
+ int cim = checkIfMatch(request, etag);\r
+ if ((cim == COND_FAILED) || (cim == COND_WEAK)) {\r
+ reply = request.makeReply(HTTP.PRECONDITION_FAILED);\r
+ reply.setContent("Pre-conditions failed.");\r
+ reply.setContentMD5(null);\r
+ return reply;\r
+ }\r
+ if ( checkIfUnmodifiedSince(request) == COND_FAILED ) {\r
+ reply = request.makeReply(HTTP.PRECONDITION_FAILED);\r
+ reply.setContent("Pre-conditions failed.");\r
+ reply.setContentMD5(null);\r
+ return reply;\r
+ }\r
+ if (checkValidators(request, etag) == COND_FAILED) {\r
+ reply = createDefaultReply(request, HTTP.NOT_MODIFIED);\r
+ reply.setETag(etag);\r
+ reply.setContentMD5(null);\r
+ return reply;\r
+ } \r
+ if (! fresource.getFile().exists()) {\r
+ return deleteMe(request);\r
+ } else {\r
+ if (commentOnly) {\r
+ reply = createCommentReply(request);\r
+ reply.setStream((InputStream) null);\r
+ } else {\r
+ reply = createDefaultReply(request, HTTP.OK);\r
+ reply.setVary(vary);\r
+ }\r
+ if (request.hasState(STATE_CONTENT_LOCATION))\r
+ reply.setContentLocation(getURL(request).toExternalForm());\r
+ return reply;\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Get for FileResource\r
+ * @param request the incomming request.\r
+ * @return A Reply instance\r
+ * @exception ProtocolException If processsing the request failed.\r
+ * @exception ResourceException If the resource got a fatal error.\r
+ */\r
+ protected Reply getFileResource(Request request) \r
+ throws ProtocolException, ResourceException\r
+ {\r
+ if (fresource == null) \r
+ throw new ResourceException("this frame is not attached to a "+\r
+ "FileResource. ("+\r
+ resource.getIdentifier()+")");\r
+ Reply reply = null;\r
+ File file = fresource.getFile() ;\r
+ fresource.checkContent();\r
+ updateCachedHeaders();\r
+ String param = null;\r
+ String sfile = request.getURL().getFile();\r
+ int pos = sfile.indexOf(';');\r
+ if (pos != -1) {\r
+ param = (String) request.getState("type");\r
+ }\r
+ if (param != null) {\r
+ HttpAccept acc[] = request.getAccept();\r
+ HttpAccept newacc[] = null;\r
+ if (acc != null) {\r
+ newacc = new HttpAccept[acc.length+1];\r
+ System.arraycopy(acc, 0, newacc, 1, acc.length);\r
+ } else {\r
+ newacc = new HttpAccept[1];\r
+ }\r
+ try {\r
+ newacc[0] = HttpFactory.makeAccept(new MimeType(param), 1.1);\r
+ request.setAccept(newacc);\r
+ } catch (MimeTypeFormatException ex) {\r
+ // not a valid mime type... maybe something else, do not care\r
+ }\r
+ }\r
+ boolean commentOnly = negotiate(request);\r
+ HttpEntityTag etag = null;\r
+ if (commentOnly)\r
+ etag = getComETag();\r
+ else\r
+ etag = getETag();\r
+ // Check validators:\r
+ int cim = checkIfMatch(request, etag);\r
+ if ((cim == COND_FAILED) || (cim == COND_WEAK)) {\r
+ reply = request.makeReply(HTTP.PRECONDITION_FAILED);\r
+ reply.setContent("Pre-conditions failed.");\r
+ reply.setContentMD5(null);\r
+ return reply;\r
+ }\r
+ if ( checkIfUnmodifiedSince(request) == COND_FAILED ) {\r
+ reply = request.makeReply(HTTP.PRECONDITION_FAILED);\r
+ reply.setContent("Pre-conditions failed.");\r
+ reply.setContentMD5(null);\r
+ return reply;\r
+ }\r
+ if ( checkValidators(request, etag) == COND_FAILED ) {\r
+ reply = createDefaultReply(request, HTTP.NOT_MODIFIED);\r
+ reply.setETag(etag);\r
+ reply.setContentMD5(null);\r
+ return reply;\r
+ }\r
+ // Does this file really exists, if so send it back\r
+ if ( file.exists() ) {\r
+ if (commentOnly) {\r
+ reply = createCommentReply(request);\r
+ } else {\r
+ reply = createFileReply(request);\r
+ }\r
+ if (request.hasState(STATE_CONTENT_LOCATION))\r
+ reply.setContentLocation(getURL(request).toExternalForm());\r
+ return reply;\r
+ } else {\r
+ return deleteMe(request);\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Allow PUT based only on ETags, otherwise PUT is done on the image itself\r
+ * @see HTTPFrame.putFileResource\r
+ */\r
+ protected Reply putFileResource(Request request)\r
+ throws ProtocolException, ResourceException\r
+ {\r
+ // check if it is the right resource below!\r
+ if (!(fresource instanceof ImageFileResource)) {\r
+ return super.putFileResource(request);\r
+ }\r
+ Reply reply = null;\r
+ int status = HTTP.OK;\r
+ fresource.checkContent();\r
+ updateCachedHeaders();\r
+ // Is this resource writable ?\r
+ if ( ! getPutableFlag() ) {\r
+ Reply error = request.makeReply(HTTP.NOT_ALLOWED) ;\r
+ error.setContent("Method PUT not allowed.") ;\r
+ throw new HTTPException (error) ;\r
+ }\r
+ HttpEntityTag etag = getComETag();\r
+ // no IfMatch, or no matching ETag, maybe a PUT on the image\r
+ int cim = checkIfMatch(request, etag);\r
+ if ((request.getIfMatch() == null) || \r
+ (cim == COND_FAILED) || (cim == COND_WEAK)) {\r
+ return super.putFileResource(request);\r
+ }\r
+ // check all the others validator\r
+\r
+ // Check remaining validators (checking if-none-match is lame\r
+ // as we already require the If-Match\r
+ if ((checkIfNoneMatch(request, etag) == COND_FAILED)\r
+ || (checkIfModifiedSince(request) == COND_FAILED)\r
+ || (checkIfUnmodifiedSince(request) == COND_FAILED)) {\r
+ Reply r = request.makeReply(HTTP.PRECONDITION_FAILED);\r
+ r.setContent("Pre-condition failed.");\r
+ return r;\r
+ }\r
+ // Check the request:\r
+ InputStream in = null;\r
+ try {\r
+ in = request.getInputStream();\r
+ if ( in == null ) {\r
+ Reply error = request.makeReply(HTTP.BAD_REQUEST) ;\r
+ error.setContent ("<p>Request doesn't have a valid content.");\r
+ throw new HTTPException (error) ;\r
+ }\r
+ } catch (IOException ex) {\r
+ throw new ClientException(request.getClient(), ex);\r
+ }\r
+ // We do not support (for the time being) put with ranges:\r
+ if ( request.hasContentRange() ) {\r
+ Reply error = request.makeReply(HTTP.BAD_REQUEST);\r
+ error.setContent("partial PUT not supported.");\r
+ throw new HTTPException(error);\r
+ }\r
+ // Check that if some type is provided it doesn't conflict:\r
+ if ( request.hasContentType() ) {\r
+ MimeType rtype = request.getContentType() ;\r
+ MimeType type = getCommentType() ;\r
+ if ( type == null ) {\r
+ setValue (ATTR_CONTENT_TYPE, rtype) ;\r
+ } else if ( rtype.match (type) < 0 ) {\r
+ if (debug) {\r
+ System.out.println("No match between: ["+\r
+ rtype.toString()+"] and ["+\r
+ type.toString()+"]");\r
+ }\r
+ Reply error = request.makeReply(HTTP.UNSUPPORTED_MEDIA_TYPE) ;\r
+ error.setContent ("<p>Invalid content type: "+type.toString());\r
+ throw new HTTPException (error) ;\r
+ }\r
+ }\r
+ ImageFileResource ifresource = (ImageFileResource) fresource;\r
+ // Write the body back to the file:\r
+ try {\r
+ // We are about to accept the put, notify client before continuing\r
+ Client client = request.getClient();\r
+ if ( client != null && request.getExpect() != null ) {\r
+ client.sendContinue();\r
+ }\r
+ if ( ifresource.newMetadataContent(request.getInputStream()) )\r
+ status = HTTP.CREATED;\r
+ else\r
+ status = HTTP.NO_CONTENT;\r
+ } catch (IOException ex) {\r
+ throw new ClientException(request.getClient(), ex);\r
+ }\r
+ if ( status == HTTP.CREATED ) {\r
+ reply = createCommentReply(request, status);\r
+ reply.setContent("<P>Resource succesfully created");\r
+ if (request.hasState(STATE_CONTENT_LOCATION))\r
+ reply.setContentLocation(getURL(request).toExternalForm());\r
+ // Henrik's fix, create the Etag on 201\r
+ if (fresource != null) {\r
+ // We only take car eof etag here:\r
+ if ( etag == null ) {\r
+ reply.setETag(getComETag());\r
+ }\r
+ }\r
+ reply.setLocation(getURL(request));\r
+ reply.setContent ("<p>Entity body saved succesfully !") ;\r
+ } else {\r
+ reply = createCommentReply(request, status);\r
+ }\r
+ return reply ;\r
+ } \r
+}\r