001: // JpegXMPFrame.java
002: // $Id: JpegXMPFrame.java,v 1.2 2003/01/27 16:14:55 ylafon Exp $
003: // (c) COPYRIGHT MIT, ERCIM and Keio, 2003.
004: // Please first read the full copyright statement in file COPYRIGHT.html
005:
006: package org.w3c.jigsaw.frames;
007:
008: import java.io.File;
009: import java.io.IOException;
010: import java.io.InputStream;
011: import java.net.URLEncoder;
012: import org.w3c.www.mime.MimeType;
013: import org.w3c.www.mime.MimeTypeFormatException;
014: import org.w3c.tools.resources.Attribute;
015: import org.w3c.tools.resources.AttributeRegistry;
016: import org.w3c.tools.resources.ProtocolException;
017: import org.w3c.tools.resources.ResourceException;
018: import org.w3c.tools.resources.event.AttributeChangedEvent;
019: import org.w3c.www.http.HTTP;
020: import org.w3c.www.http.HttpAccept;
021: import org.w3c.www.http.HttpEntityTag;
022: import org.w3c.www.http.HttpFactory;
023: import org.w3c.www.http.HttpInvalidValueException;
024: import org.w3c.jigsaw.http.Client;
025: import org.w3c.jigsaw.http.ClientException;
026: import org.w3c.jigsaw.http.HTTPException;
027: import org.w3c.jigsaw.http.Reply;
028: import org.w3c.jigsaw.http.Request;
029:
030: import org.w3c.tools.jpeg.JpegHeaders;
031: import org.w3c.jigsaw.resources.ImageFileResource;
032:
033: /**
034: * This class will read the XMP marker from a jpeg file and return it
035: * depending on the Accept: header
036: */
037:
038: public class JpegXMPFrame extends HTTPFrame {
039: public static final boolean debug = false;
040: /**
041: * Attribute index - The comment content type
042: */
043:
044: static {
045: Attribute a = null;
046: Class cls = null;
047: try {
048: cls = Class.forName("org.w3c.jigsaw.frames.JpegXMPFrame");
049: } catch (Exception ex) {
050: ex.printStackTrace();
051: System.exit(1);
052: }
053: }
054:
055: /**
056: * the static String of the Vary ehader to be added
057: */
058: protected static String[] vary = { "Accept" };
059:
060: /**
061: * the static String of the Vary ehader to be added
062: */
063: protected static MimeType xmptype = MimeType.APPLICATION_RDF_XML;
064:
065: /**
066: * The comment entity tag
067: */
068: protected HttpEntityTag xmpetag = null;
069:
070: /**
071: * The XMP.
072: */
073: protected String xmpinfo = null;
074:
075: /**
076: * Extract the XMP from the jpeg image.
077: * @return the xmp info
078: */
079: protected String getMetadata() {
080: if (fresource == null)
081: return null;
082: File file = fresource.getFile();
083: if (file.exists()) {
084: String comments[] = null;
085: try {
086: JpegHeaders headers = new JpegHeaders(file);
087: xmpinfo = headers.getXMP();
088: } catch (Exception ex) {
089: ex.printStackTrace();
090: return "unable to get XMP: " + ex.getMessage();
091: }
092: }
093: return xmpinfo;
094: }
095:
096: /**
097: * Get the comment Etag
098: * @return an instance of HttpEntityTag, or <strong>null</strong> if not
099: * defined.
100: */
101:
102: public HttpEntityTag getXMPETag() {
103: if (xmpetag == null) {
104: String etag_s = null;
105: if (fresource != null) {
106: long lstamp = fresource.getFileStamp() + 1;
107: if (lstamp >= 0L) {
108: String soid = Integer.toString(getOid(), 32);
109: String stamp = Long.toString(lstamp, 32);
110: etag_s = Integer.toString(getOid(), 32) + ":"
111: + Long.toString(lstamp, 32);
112: }
113: }
114: xmpetag = HttpFactory.makeETag(false, etag_s);
115: }
116: return xmpetag;
117: }
118:
119: /**
120: * Update the cached headers value.
121: * Each resource maintains a set of cached values for headers, this
122: * allows for a nice sped-up in headers marshalling, which - as the
123: * complexity of the protocol increases - becomes a bottleneck.
124: */
125:
126: protected void updateCachedHeaders() {
127: super .updateCachedHeaders();
128: if (xmpinfo == null) {
129: xmpinfo = getMetadata();
130: }
131: }
132:
133: public Reply createXMPReply(Request request, int status) {
134: Reply reply = request.makeReply(status);
135: updateCachedHeaders();
136: reply.setContent(xmpinfo);
137: reply.setContentType(xmptype);
138: reply.setVary(vary);
139: if (lastmodified != null)
140: reply.setHeaderValue(Reply.H_LAST_MODIFIED, lastmodified);
141: if (contentencoding != null)
142: reply.setHeaderValue(Reply.H_CONTENT_ENCODING,
143: contentencoding);
144: if (contentlanguage != null)
145: reply.setHeaderValue(Reply.H_CONTENT_LANGUAGE,
146: contentlanguage);
147: long maxage = getMaxAge();
148: if (maxage >= 0) {
149: if (reply.getMajorVersion() >= 1) {
150: if (reply.getMinorVersion() >= 1) {
151: reply.setMaxAge((int) (maxage / 1000));
152: }
153: // If max-age is zero, say what you mean:
154: long expires = (System.currentTimeMillis() + ((maxage == 0) ? -1000
155: : maxage));
156: reply.setExpires(expires);
157: }
158: }
159: // Set the date of the reply (round it to secs):
160: reply.setDate((System.currentTimeMillis() / 1000L) * 1000L);
161: reply.setETag(getXMPETag());
162: reply.setContentLocation(getURL(request).toExternalForm() + ";"
163: + URLEncoder.encode(xmptype.toString()));
164: return reply;
165: }
166:
167: public Reply createXMPReply(Request request) {
168: return createXMPReply(request, HTTP.OK);
169: }
170:
171: /**
172: * Check the <code>If-Match</code> condition of that request.
173: * @param request The request to check.
174: * @return An integer, either <code>COND_FAILED</cond> if condition
175: * was checked, but failed, <code>COND_OK</code> if condition was checked
176: * and succeeded, or <strong>0</strong> if the condition was not checked
177: * at all (eg because the resource or the request didn't support it).
178: */
179:
180: public int checkIfMatch(Request request, HttpEntityTag etag) {
181: if (fresource != null) {
182: HttpEntityTag tags[] = request.getIfMatch();
183: if (tags != null) {
184: // Good, real validators in use:
185: if (etag != null) {
186: // Note: if etag is null this means that the resource has
187: // changed and has not been even emited since then...
188: for (int i = 0; i < tags.length; i++) {
189: HttpEntityTag t = tags[i];
190: if (t.getTag().equals(etag.getTag())) {
191: if (t.isWeak() || etag.isWeak()) {
192: return COND_WEAK;
193: } else {
194: return COND_OK;
195: }
196: }
197: }
198: }
199: return COND_FAILED;
200: }
201: }
202: return 0;
203: }
204:
205: /**
206: * Check the <code>If-None-Match</code> condition of that request.
207: * @param request The request to check.
208: * @return An integer, either <code>COND_FAILED</cond> if condition
209: * was checked, but failed, <code>COND_OK</code> if condition was checked
210: * and succeeded, or <strong>0</strong> if the condition was not checked
211: * at all (eg because the resource or the request didn't support it).
212: */
213:
214: public int checkIfNoneMatch(Request request, HttpEntityTag etag) {
215: if (fresource != null) {
216: // Check for an If-None-Match conditional:
217: HttpEntityTag tags[] = request.getIfNoneMatch();
218: if (tags != null) {
219: if (etag == null) {
220: return COND_OK;
221: }
222: int status = COND_OK;
223: for (int i = 0; i < tags.length; i++) {
224: HttpEntityTag t = tags[i];
225: if (t.getTag().equals(etag.getTag())) {
226: if (t.isWeak() || etag.isWeak()) {
227: status = COND_WEAK;
228: } else {
229: return COND_FAILED;
230: }
231: }
232: if (t.getTag().equals("*")) {
233: if (fresource != null) {
234: File f = fresource.getFile();
235: if (f.exists()) {
236: return COND_FAILED;
237: }
238: } else {
239: return COND_FAILED;
240: }
241: }
242: }
243: return status;
244: }
245: }
246: return 0;
247: }
248:
249: /**
250: * check the validators namely LMT/Etags according to rfc2616 rules
251: * @return An integer, either <code>COND_FAILED</cond> if condition
252: * was checked, but failed, <code>COND_OK</code> if condition was checked
253: * and succeeded, or <strong>0</strong> if the condition was not checked
254: * at all (eg because the resource or the request didn't support it).
255: */
256: public int checkValidators(Request request, HttpEntityTag etag) {
257: int v_inm = checkIfNoneMatch(request, etag);
258: int v_ims = checkIfModifiedSince(request);
259:
260: if ((v_inm == COND_OK) || (v_ims == COND_OK)) {
261: return COND_OK;
262: }
263: if ((v_inm == COND_FAILED) || (v_ims == COND_FAILED)) {
264: return COND_FAILED;
265: }
266: if ((v_inm == COND_WEAK) || (v_ims == COND_WEAK)) {
267: return COND_OK;
268: }
269: return 0;
270: }
271:
272: /**
273: * Negotiate.
274: * @param request the incomming request.
275: * @return true if the client wants the comment, false if the client
276: * wants the image.
277: */
278: protected boolean negotiate(Request request)
279: throws ProtocolException {
280: if (!request.hasAccept()) {
281: //return the image
282: return false;
283: } else {
284: // The browser has given some preferences:
285: HttpAccept accepts[] = request.getAccept();
286:
287: //two content types image/jpeg and comment-type
288: HttpAccept imgAccept = getMatchingAccept(accepts,
289: getContentType());
290: HttpAccept xmpAccept = getMatchingAccept(accepts, xmptype);
291:
292: if ((imgAccept != null) && (xmpAccept != null)) {
293: // go for best MIME match first
294: int matchImg = getContentType().match(
295: imgAccept.getMimeType());
296: int matchXMP = xmptype.match(xmpAccept.getMimeType());
297:
298: if (matchImg == matchXMP) {
299: // equals, use quality
300: return (imgAccept.getQuality() < xmpAccept
301: .getQuality());
302: } else {
303: return (matchImg < matchXMP);
304: }
305: } else if (xmpAccept != null)
306: return true;
307: else
308: return false;
309: }
310: }
311:
312: protected HttpAccept getMatchingAccept(HttpAccept accepts[],
313: MimeType mime) {
314: int jmatch = -1;
315: int jidx = -1;
316: for (int i = 0; i < accepts.length; i++) {
317: try {
318: int match = mime.match(accepts[i].getMimeType());
319: if (match > jmatch) {
320: jmatch = match;
321: jidx = i;
322: }
323: } catch (HttpInvalidValueException ivex) {
324: // There is a bad acept header here
325: // let's be cool and ignore it
326: // FIXME we should answer with a Bad Request
327: }
328: }
329: if (jidx < 0)
330: return null;
331: return accepts[jidx];
332: }
333:
334: /**
335: * Perform a HEAD request for the associated FileResource.
336: * @param request the incomming request.
337: * @return A Reply instance
338: * @exception ProtocolException If processsing the request failed.
339: * @exception ResourceException If the resource got a fatal error.
340: */
341: protected Reply headFileResource(Request request)
342: throws ProtocolException, ResourceException {
343: if (fresource == null)
344: throw new ResourceException(
345: "this frame is not attached to a "
346: + "FileResource. ("
347: + resource.getIdentifier() + ")");
348: Reply reply = null;
349: fresource.checkContent();
350: updateCachedHeaders();
351: // hack, if ;text/html is there,
352: // it will be added at first place of the accept
353: String param = null;
354: String sfile = request.getURL().getFile();
355: int pos = sfile.indexOf(';');
356: if (pos != -1) {
357: param = (String) request.getState("type");
358: }
359: if (param != null) {
360: HttpAccept acc[] = request.getAccept();
361: HttpAccept newacc[] = null;
362: if (acc != null) {
363: newacc = new HttpAccept[acc.length + 1];
364: System.arraycopy(acc, 0, newacc, 1, acc.length);
365: } else {
366: newacc = new HttpAccept[1];
367: }
368: try {
369: newacc[0] = HttpFactory.makeAccept(new MimeType(param),
370: 1.1);
371: request.setAccept(newacc);
372: } catch (MimeTypeFormatException ex) {
373: // not a valid mime type... maybe something else, do not care
374: }
375: }
376: boolean xmpOnly = negotiate(request);
377: HttpEntityTag etag = null;
378: if (xmpOnly)
379: etag = getXMPETag();
380: else
381: etag = getETag();
382: // Check validators:
383: int cim = checkIfMatch(request, etag);
384: if ((cim == COND_FAILED) || (cim == COND_WEAK)) {
385: reply = request.makeReply(HTTP.PRECONDITION_FAILED);
386: reply.setContent("Pre-conditions failed.");
387: reply.setContentMD5(null);
388: return reply;
389: }
390: if (checkIfUnmodifiedSince(request) == COND_FAILED) {
391: reply = request.makeReply(HTTP.PRECONDITION_FAILED);
392: reply.setContent("Pre-conditions failed.");
393: reply.setContentMD5(null);
394: return reply;
395: }
396: if (checkValidators(request, etag) == COND_FAILED) {
397: reply = createDefaultReply(request, HTTP.NOT_MODIFIED);
398: reply.setETag(etag);
399: reply.setContentMD5(null);
400: return reply;
401: }
402: if (!fresource.getFile().exists()) {
403: return deleteMe(request);
404: } else {
405: if (xmpOnly) {
406: reply = createXMPReply(request);
407: reply.setStream((InputStream) null);
408: } else {
409: reply = createDefaultReply(request, HTTP.OK);
410: reply.setVary(vary);
411: }
412: if (request.hasState(STATE_CONTENT_LOCATION))
413: reply.setContentLocation(getURL(request)
414: .toExternalForm());
415: return reply;
416: }
417: }
418:
419: /**
420: * Get for FileResource
421: * @param request the incomming request.
422: * @return A Reply instance
423: * @exception ProtocolException If processsing the request failed.
424: * @exception ResourceException If the resource got a fatal error.
425: */
426: protected Reply getFileResource(Request request)
427: throws ProtocolException, ResourceException {
428: if (fresource == null)
429: throw new ResourceException(
430: "this frame is not attached to a "
431: + "FileResource. ("
432: + resource.getIdentifier() + ")");
433: Reply reply = null;
434: File file = fresource.getFile();
435: fresource.checkContent();
436: updateCachedHeaders();
437: String param = null;
438: String sfile = request.getURL().getFile();
439: int pos = sfile.indexOf(';');
440: if (pos != -1) {
441: param = (String) request.getState("type");
442: }
443: if (param != null) {
444: HttpAccept acc[] = request.getAccept();
445: HttpAccept newacc[] = null;
446: if (acc != null) {
447: newacc = new HttpAccept[acc.length + 1];
448: System.arraycopy(acc, 0, newacc, 1, acc.length);
449: } else {
450: newacc = new HttpAccept[1];
451: }
452: try {
453: newacc[0] = HttpFactory.makeAccept(new MimeType(param),
454: 1.1);
455: request.setAccept(newacc);
456: } catch (MimeTypeFormatException ex) {
457: // not a valid mime type... maybe something else, do not care
458: }
459: }
460: boolean xmpOnly = negotiate(request);
461: HttpEntityTag etag = null;
462: if (xmpOnly)
463: etag = getXMPETag();
464: else
465: etag = getETag();
466: // Check validators:
467: int cim = checkIfMatch(request, etag);
468: if ((cim == COND_FAILED) || (cim == COND_WEAK)) {
469: reply = request.makeReply(HTTP.PRECONDITION_FAILED);
470: reply.setContent("Pre-conditions failed.");
471: reply.setContentMD5(null);
472: return reply;
473: }
474: if (checkIfUnmodifiedSince(request) == COND_FAILED) {
475: reply = request.makeReply(HTTP.PRECONDITION_FAILED);
476: reply.setContent("Pre-conditions failed.");
477: reply.setContentMD5(null);
478: return reply;
479: }
480: if (checkValidators(request, etag) == COND_FAILED) {
481: reply = createDefaultReply(request, HTTP.NOT_MODIFIED);
482: reply.setETag(etag);
483: reply.setContentMD5(null);
484: return reply;
485: }
486: // Does this file really exists, if so send it back
487: if (file.exists()) {
488: if (xmpOnly) {
489: reply = createXMPReply(request);
490: } else {
491: reply = createFileReply(request);
492: }
493: if (request.hasState(STATE_CONTENT_LOCATION))
494: reply.setContentLocation(getURL(request)
495: .toExternalForm());
496: return reply;
497: } else {
498: return deleteMe(request);
499: }
500: }
501:
502: /**
503: * Allow PUT based only on ETags, otherwise PUT is done on the image itself
504: * @see HTTPFrame.putFileResource
505: */
506: protected Reply putFileResource(Request request)
507: throws ProtocolException, ResourceException {
508: // check if it is the right resource below!
509: if (!(fresource instanceof ImageFileResource)) {
510: return super .putFileResource(request);
511: }
512: Reply reply = null;
513: int status = HTTP.OK;
514: fresource.checkContent();
515: updateCachedHeaders();
516: // Is this resource writable ?
517: if (!getPutableFlag()) {
518: Reply error = request.makeReply(HTTP.NOT_ALLOWED);
519: error.setContent("Method PUT not allowed.");
520: throw new HTTPException(error);
521: }
522: HttpEntityTag etag = getXMPETag();
523: // no IfMatch, or no matching ETag, maybe a PUT on the image
524: int cim = checkIfMatch(request, etag);
525: if ((request.getIfMatch() == null) || (cim == COND_FAILED)
526: || (cim == COND_WEAK)) {
527: return super .putFileResource(request);
528: }
529: // check all the others validator
530:
531: // Check remaining validators (checking if-none-match is lame
532: // as we already require the If-Match
533: if ((checkIfNoneMatch(request, etag) == COND_FAILED)
534: || (checkIfModifiedSince(request) == COND_FAILED)
535: || (checkIfUnmodifiedSince(request) == COND_FAILED)) {
536: Reply r = request.makeReply(HTTP.PRECONDITION_FAILED);
537: r.setContent("Pre-condition failed.");
538: return r;
539: }
540: // Check the request:
541: InputStream in = null;
542: try {
543: in = request.getInputStream();
544: if (in == null) {
545: Reply error = request.makeReply(HTTP.BAD_REQUEST);
546: error
547: .setContent("<p>Request doesn't have a valid content.");
548: throw new HTTPException(error);
549: }
550: } catch (IOException ex) {
551: throw new ClientException(request.getClient(), ex);
552: }
553: // We do not support (for the time being) put with ranges:
554: if (request.hasContentRange()) {
555: Reply error = request.makeReply(HTTP.BAD_REQUEST);
556: error.setContent("partial PUT not supported.");
557: throw new HTTPException(error);
558: }
559: // Check that if some type is provided it doesn't conflict:
560: if (request.hasContentType()) {
561: MimeType rtype = request.getContentType();
562: MimeType type = xmptype;
563: if (type == null) {
564: setValue(ATTR_CONTENT_TYPE, rtype);
565: } else if (rtype.match(type) < 0) {
566: if (debug) {
567: System.out.println("No match between: ["
568: + rtype.toString() + "] and ["
569: + type.toString() + "]");
570: }
571: Reply error = request
572: .makeReply(HTTP.UNSUPPORTED_MEDIA_TYPE);
573: error.setContent("<p>Invalid content type: "
574: + type.toString());
575: throw new HTTPException(error);
576: }
577: }
578: ImageFileResource ifresource = (ImageFileResource) fresource;
579: // Write the body back to the file:
580: try {
581: // We are about to accept the put, notify client before continuing
582: Client client = request.getClient();
583: if (client != null && request.getExpect() != null) {
584: client.sendContinue();
585: }
586: if (ifresource.newMetadataContent(request.getInputStream()))
587: status = HTTP.CREATED;
588: else
589: status = HTTP.NO_CONTENT;
590: } catch (IOException ex) {
591: throw new ClientException(request.getClient(), ex);
592: }
593: if (status == HTTP.CREATED) {
594: reply = createXMPReply(request, status);
595: reply.setContent("<P>Resource succesfully created");
596: if (request.hasState(STATE_CONTENT_LOCATION))
597: reply.setContentLocation(getURL(request)
598: .toExternalForm());
599: // Henrik's fix, create the Etag on 201
600: if (fresource != null) {
601: // We only take car eof etag here:
602: if (etag == null) {
603: reply.setETag(getXMPETag());
604: }
605: }
606: reply.setLocation(getURL(request));
607: reply.setContent("<p>Entity body saved succesfully !");
608: } else {
609: reply = createXMPReply(request, status);
610: }
611: return reply;
612: }
613: }
|