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