001: /*
002: * Copyright 2005-2007 Noelios Consulting.
003: *
004: * The contents of this file are subject to the terms of the Common Development
005: * and Distribution License (the "License"). You may not use this file except in
006: * compliance with the License.
007: *
008: * You can obtain a copy of the license at
009: * http://www.opensource.org/licenses/cddl1.txt See the License for the specific
010: * language governing permissions and limitations under the License.
011: *
012: * When distributing Covered Code, include this CDDL HEADER in each file and
013: * include the License file at http://www.opensource.org/licenses/cddl1.txt If
014: * applicable, add the following below this CDDL HEADER, with the fields
015: * enclosed by brackets "[]" replaced with your own identifying information:
016: * Portions Copyright [yyyy] [name of copyright owner]
017: */
018:
019: package com.noelios.restlet.local;
020:
021: import java.io.IOException;
022: import java.util.Comparator;
023: import java.util.List;
024: import java.util.Set;
025: import java.util.SortedSet;
026: import java.util.TreeSet;
027: import java.util.logging.Level;
028:
029: import org.restlet.Directory;
030: import org.restlet.Uniform;
031: import org.restlet.data.MediaType;
032: import org.restlet.data.Method;
033: import org.restlet.data.Preference;
034: import org.restlet.data.Reference;
035: import org.restlet.data.ReferenceList;
036: import org.restlet.data.Request;
037: import org.restlet.data.Response;
038: import org.restlet.data.Status;
039: import org.restlet.resource.Representation;
040: import org.restlet.resource.Resource;
041: import org.restlet.resource.Variant;
042:
043: /**
044: * Resource supported by a set of context representations (from file system,
045: * class loaders and webapp context). A content negotiation mechanism (similar
046: * to Apache HTTP server) is available. It is based on path extensions to detect
047: * variants (languages, media types or character sets).
048: *
049: * @see <a
050: * href="http://httpd.apache.org/docs/2.0/content-negotiation.html">Apache
051: * mod_negotiation module</a>
052: * @author Jerome Louvel (contact@noelios.com)
053: * @author Thierry Boileau
054: */
055: public class DirectoryResource extends Resource {
056:
057: /** The parent directory handler. */
058: private Directory directory;
059:
060: /** The resource path relative to the directory URI. */
061: private String relativePart;
062:
063: /** The context's target URI (file, clap URI). */
064: private String targetUri;
065:
066: /** Indicates if the target resource is a directory. */
067: private boolean targetDirectory;
068:
069: /** Indicates if the target resource is a file. */
070: private boolean targetFile;
071:
072: /** Indicates if the target resource is a directory with an index. */
073: private boolean targetIndex;
074:
075: /** The context's directory URI (file, clap URI). */
076: private String directoryUri;
077:
078: /**
079: * The local base name of the resource. For example, "foo.en" and
080: * "foo.en-GB.html" return "foo".
081: */
082: private String baseName;
083:
084: /** The base set of extensions. */
085: private Set<String> baseExtensions;
086:
087: /** The unique representation of the target URI, if it exists. */
088: private Reference uniqueReference;
089:
090: /** If the resource is a directory, this contains its content. */
091: private ReferenceList directoryContent;
092:
093: /** If the resource is a file, this contains its content. */
094: private Representation fileContent;
095:
096: /**
097: * If the resource is a directory, the non-trailing slash caracter leads to
098: * redirection.
099: */
100: private boolean directoryRedirection;
101:
102: /**
103: * Constructor.
104: *
105: * @param directory
106: * The parent directory handler.
107: * @param request
108: * The handled call.
109: * @throws IOException
110: */
111: public DirectoryResource(Directory directory, Request request,
112: Response response) throws IOException {
113: super (directory.getContext(), request, response);
114:
115: // Update the member variables
116: this .directory = directory;
117: this .relativePart = request.getResourceRef().getRemainingPart();
118:
119: if (this .relativePart.startsWith("/")) {
120: // We enforce the leading slash on the root URI
121: this .relativePart = this .relativePart.substring(1);
122: }
123:
124: // The target uri does not take into account the query and fragment
125: // parts of the resource.
126: this .targetUri = new Reference(directory.getRootRef()
127: .toString()
128: + this .relativePart).normalize().toString(false, false);
129: if (!this .targetUri.startsWith(directory.getRootRef()
130: .toString())) {
131: // Prevent the client from accessing resources in upper directories
132: this .targetUri = directory.getRootRef().toString();
133: }
134:
135: // Try to detect the presence of a directory
136: Response contextResponse = getDispatcher().get(this .targetUri);
137: if (contextResponse.getEntity() != null) {
138: // As a convention, underlying client connectors return the
139: // directory listing with the media-type "MediaType.TEXT_URI_LIST"
140: // when handling directories
141: if (MediaType.TEXT_URI_LIST.equals(contextResponse
142: .getEntity().getMediaType())) {
143: this .targetDirectory = true;
144: this .targetFile = false;
145: this .directoryContent = new ReferenceList(
146: contextResponse.getEntity());
147: if (!request.getResourceRef().getIdentifier().endsWith(
148: "/")) {
149: // All requests will be automatically redirected
150: this .directoryRedirection = true;
151: }
152:
153: if (!this .targetUri.endsWith("/")) {
154: this .targetUri += "/";
155: this .relativePart += "/";
156: }
157:
158: // Append the index name
159: if (getDirectory().getIndexName() != null
160: && getDirectory().getIndexName().length() > 0) {
161: this .directoryUri = this .targetUri;
162: this .baseName = getDirectory().getIndexName();
163: this .targetUri = this .directoryUri + this .baseName;
164: this .targetIndex = true;
165: } else {
166: this .directoryUri = this .targetUri;
167: this .baseName = null;
168: }
169: } else {
170: this .targetDirectory = false;
171: this .targetFile = true;
172: this .fileContent = contextResponse.getEntity();
173: }
174: } else {
175: this .targetDirectory = false;
176: this .targetFile = false;
177:
178: // Let's try with the facultative index, in case the underlying
179: // client connector does not handle directory listing.
180: if (this .targetUri.endsWith("/")) {
181: // In this case, the trailing "/" shows that the URIs must
182: // points to a directory
183: if (getDirectory().getIndexName() != null
184: && getDirectory().getIndexName().length() > 0) {
185: this .directoryUri = this .targetUri;
186: this .baseName = getDirectory().getIndexName();
187: this .targetUri = this .directoryUri + this .baseName;
188: contextResponse = getDispatcher().get(
189: this .targetUri);
190: if (contextResponse.getEntity() != null) {
191: this .targetDirectory = true;
192: this .directoryContent = new ReferenceList();
193: this .directoryContent.add(new Reference(
194: this .targetUri));
195: this .targetIndex = true;
196: }
197: }
198: } else {
199: // Try to determine if this target URI with no trailing "/" is a
200: // directory, in order to force the redirection.
201: if (getDirectory().getIndexName() != null
202: && getDirectory().getIndexName().length() > 0) {
203: // Append the index name
204: contextResponse = getDispatcher().get(
205: this .targetUri + "/"
206: + getDirectory().getIndexName());
207: if (contextResponse.getEntity() != null) {
208: this .directoryUri = this .targetUri + "/";
209: this .baseName = getDirectory().getIndexName();
210: this .targetUri = this .directoryUri
211: + this .baseName;
212: this .targetDirectory = true;
213: this .directoryRedirection = true;
214: this .directoryContent = new ReferenceList();
215: this .directoryContent.add(new Reference(
216: this .targetUri));
217: this .targetIndex = true;
218: }
219: }
220: }
221: }
222:
223: if (!this .targetDirectory) {
224: int lastSlashIndex = targetUri.lastIndexOf('/');
225: if (lastSlashIndex == -1) {
226: this .directoryUri = "";
227: this .baseName = targetUri;
228: } else {
229: this .directoryUri = targetUri.substring(0,
230: lastSlashIndex + 1);
231: this .baseName = targetUri.substring(lastSlashIndex + 1);
232: }
233:
234: contextResponse = getDispatcher().get(this .directoryUri);
235: if ((contextResponse.getEntity() != null)
236: && MediaType.TEXT_URI_LIST.equals(contextResponse
237: .getEntity().getMediaType())) {
238: this .directoryContent = new ReferenceList(
239: contextResponse.getEntity());
240: }
241: }
242:
243: if (this .baseName != null) {
244: // Remove the extensions from the base name
245: int firstDotIndex = this .baseName.indexOf('.');
246: if (firstDotIndex != -1) {
247: // Store the set of extensions
248: this .baseExtensions = getExtensions(this .baseName);
249:
250: // Remove stored extensions from the base name
251: this .baseName = this .baseName.substring(0,
252: firstDotIndex);
253: }
254:
255: }
256:
257: // Log results
258: getLogger().info("Converted base path: " + this .targetUri);
259: getLogger().info("Converted base name: " + this .baseName);
260: }
261:
262: /**
263: * Indicates if it is allowed to delete the resource. The default value is
264: * false.
265: *
266: * @return True if the method is allowed.
267: */
268: public boolean allowDelete() {
269: return getDirectory().isModifiable();
270: }
271:
272: /**
273: * Indicates if it is allowed to put to the resource. The default value is
274: * false.
275: *
276: * @return True if the method is allowed.
277: */
278: public boolean allowPut() {
279: return getDirectory().isModifiable();
280: }
281:
282: @Override
283: public void handleGet() {
284: if (directoryRedirection) {
285: // If this request targets a directory and if the target URI does
286: // not end with a tailing "/", the client is told to redirect to a
287: // correct URI.
288: getResponse()
289: .redirectPermanent(
290: getRequest().getResourceRef()
291: .getIdentifier()
292: + "/");
293: } else {
294: super .handleGet();
295: }
296:
297: }
298:
299: /**
300: * Asks the resource to delete itself and all its representations.
301: */
302: public void delete() {
303: Status status;
304:
305: if (directoryRedirection && !targetIndex) {
306: getResponse().setStatus(Status.REDIRECTION_SEE_OTHER);
307: getResponse().setRedirectRef(this .targetUri);
308: } else {
309: // We allow the transfer of the PUT calls only if the readOnly flag
310: // is not set
311: if (!getDirectory().isModifiable()) {
312: status = new Status(Status.CLIENT_ERROR_FORBIDDEN,
313: "No modification allowed.");
314: } else {
315: Request contextRequest = new Request(Method.DELETE,
316: this .targetUri);
317: Response contextResponse = new Response(contextRequest);
318:
319: if (targetDirectory && !targetIndex) {
320: contextRequest.setResourceRef(this .targetUri);
321: getDispatcher().handle(contextRequest,
322: contextResponse);
323: } else {
324: // Check if there is only one representation
325:
326: // Try to get the unique representation of the resource
327: ReferenceList references = getVariantsReferences();
328: if (!references.isEmpty()) {
329: if (uniqueReference != null) {
330: contextRequest
331: .setResourceRef(uniqueReference);
332: getDispatcher().handle(contextRequest,
333: contextResponse);
334: } else {
335: // We found variants, but not the right one
336: contextResponse
337: .setStatus(new Status(
338: Status.CLIENT_ERROR_NOT_ACCEPTABLE,
339: "Unable to process properly the request. Several variants exist but none of them suits precisely. "));
340: }
341: } else {
342: contextResponse
343: .setStatus(Status.CLIENT_ERROR_NOT_FOUND);
344: }
345: }
346:
347: status = contextResponse.getStatus();
348: }
349:
350: getResponse().setStatus(status);
351: }
352: }
353:
354: /**
355: * Puts a variant representation in the resource.
356: *
357: * @param variant
358: * A new or updated variant representation.
359: */
360: public void put(Representation variant) {
361: Status status;
362:
363: if (directoryRedirection && !targetIndex) {
364: getResponse().setStatus(Status.REDIRECTION_SEE_OTHER);
365: getResponse().setRedirectRef(this .targetUri);
366: }
367:
368: // We allow the transfer of the PUT calls only if the readOnly flag is
369: // not set
370: if (!getDirectory().isModifiable()) {
371: status = new Status(Status.CLIENT_ERROR_FORBIDDEN,
372: "No modification allowed.");
373: } else {
374: Request contextRequest = new Request(Method.PUT,
375: this .targetUri);
376: contextRequest.setEntity(variant);
377: Response contextResponse = new Response(contextRequest);
378: contextRequest.setResourceRef(this .targetUri);
379: getDispatcher().handle(contextRequest, contextResponse);
380: status = contextResponse.getStatus();
381: }
382:
383: getResponse().setStatus(status);
384: }
385:
386: /**
387: * Returns the local base name of the file. For example, "foo.en" and
388: * "foo.en-GB.html" return "foo".
389: *
390: * @return The local name of the file.
391: */
392: public String getBaseName() {
393: return this .baseName;
394: }
395:
396: /**
397: * Returns the parent directory handler.
398: *
399: * @return The parent directory handler.
400: */
401: public Directory getDirectory() {
402: return this .directory;
403: }
404:
405: /**
406: * Returns the context's directory URI (file, clap URI).
407: *
408: * @return The context's directory URI (file, clap URI).
409: */
410: public String getDirectoryUri() {
411: return this .directoryUri;
412: }
413:
414: /**
415: * Returns a call dispatcher.
416: *
417: * @return A call dispatcher.
418: */
419: private Uniform getDispatcher() {
420: return getDirectory().getContext().getDispatcher();
421: }
422:
423: /**
424: * Returns the context's target URI (file, clap URI).
425: *
426: * @return The context's target URI (file, clap URI).
427: */
428: public String getTargetUri() {
429: return this .targetUri;
430: }
431:
432: /**
433: * Returns the representation variants.
434: *
435: * @return The representation variants.
436: */
437: public List<Variant> getVariants() {
438: List<Variant> results = super .getVariants();
439:
440: getLogger().info("Getting variants for : " + getTargetUri());
441:
442: if ((this .directoryContent != null)
443: && (getRequest().getResourceRef() != null)
444: && (getRequest().getResourceRef().getBaseRef() != null)) {
445:
446: // Allows to sort the list of representations
447: SortedSet<Representation> resultSet = new TreeSet<Representation>(
448: getRepresentationsComparator());
449:
450: // Compute the base reference (from a call's client point of view)
451: String baseRef = getRequest().getResourceRef().getBaseRef()
452: .toString(false, false);
453:
454: if (!baseRef.endsWith("/")) {
455: baseRef += "/";
456: }
457:
458: int lastIndex = this .relativePart.lastIndexOf("/");
459:
460: if (lastIndex != -1) {
461: baseRef += this .relativePart.substring(0, lastIndex);
462: }
463:
464: int rootLength = getDirectoryUri().length();
465:
466: if (this .baseName != null) {
467: String filePath;
468: for (Reference ref : getVariantsReferences()) {
469: // Add the new variant to the result list
470: Response contextResponse = getDispatcher().get(
471: ref.toString());
472: if (contextResponse.getStatus().isSuccess()
473: && (contextResponse.getEntity() != null)) {
474: filePath = ref.toString(false, false)
475: .substring(rootLength);
476: Representation rep = contextResponse
477: .getEntity();
478: rep.setIdentifier(baseRef + filePath);
479: resultSet.add(rep);
480: }
481: }
482: }
483:
484: results.addAll(resultSet);
485:
486: if (resultSet.isEmpty()) {
487: if (this .targetDirectory
488: && getDirectory().isListingAllowed()) {
489: ReferenceList userList = new ReferenceList(
490: this .directoryContent.size());
491: // Set the list identifier
492: userList.setIdentifier(baseRef);
493:
494: SortedSet<Reference> sortedSet = new TreeSet<Reference>(
495: getReferencesComparator());
496: sortedSet.addAll(this .directoryContent);
497:
498: for (Reference ref : sortedSet) {
499: String filePart = ref.toString(false, false)
500: .substring(rootLength);
501: StringBuilder filePath = new StringBuilder();
502: if ((!baseRef.endsWith("/"))
503: && (!filePart.startsWith("/"))) {
504: filePath.append('/');
505: }
506: filePath.append(filePart);
507: userList.add(baseRef + filePath);
508: }
509: List<Variant> list = getDirectory()
510: .getIndexVariants(userList);
511: for (Variant variant : list) {
512: results.add(getDirectory()
513: .getIndexRepresentation(variant,
514: userList));
515: }
516:
517: }
518: }
519: } else if (this .targetFile && (this .fileContent != null)) {
520: results.add(this .fileContent);
521: }
522:
523: return results;
524: }
525:
526: /**
527: * Allows to sort the list of representations set by the resource.
528: *
529: * @return A Comparator instance imposing a sort order of representations or
530: * null if no special order is wanted.
531: */
532: private Comparator<Representation> getRepresentationsComparator() {
533: // Sort the list of representations by their identifier.
534: Comparator<Representation> identifiersComparator = new Comparator<Representation>() {
535: public int compare(Representation rep0, Representation rep1) {
536: boolean bRep0Null = (rep0.getIdentifier() == null);
537: boolean bRep1Null = (rep1.getIdentifier() == null);
538:
539: if (bRep0Null && bRep1Null) {
540: return 0;
541: } else {
542: if (bRep0Null) {
543: return -1;
544: } else {
545: if (bRep1Null) {
546: return 1;
547: } else {
548: return rep0.getIdentifier()
549: .getLastSegment().compareTo(
550: rep1.getIdentifier()
551: .getLastSegment());
552: }
553: }
554: }
555: }
556: };
557: return identifiersComparator;
558: }
559:
560: /**
561: * Allows to sort the list of references set by the resource.
562: *
563: * @return A Comparator instance imposing a sort order of references or null
564: * if no special order is wanted.
565: */
566: private Comparator<Reference> getReferencesComparator() {
567: // Sort the list of references by their identifier.
568: Comparator<Reference> identifiersComparator = new Comparator<Reference>() {
569: public int compare(Reference rep0, Reference rep1) {
570: boolean bRep0Null = (rep0.getIdentifier() == null);
571: boolean bRep1Null = (rep1.getIdentifier() == null);
572:
573: if (bRep0Null && bRep1Null) {
574: return 0;
575: } else {
576: if (bRep0Null) {
577: return -1;
578: } else {
579: if (bRep1Null) {
580: return 1;
581: } else {
582: return rep0
583: .toString(false, false)
584: .compareTo(
585: rep1.toString(false, false));
586: }
587: }
588: }
589: }
590: };
591: return identifiersComparator;
592: }
593:
594: /**
595: * Returns the references of the representations of the target resource
596: * according to the directory handler property
597: *
598: * @return The list of variants references
599: */
600: private ReferenceList getVariantsReferences() {
601: uniqueReference = null;
602: ReferenceList result = new ReferenceList(0);
603: try {
604: Request contextCall = new Request(Method.GET,
605: this .targetUri);
606: // Ask for the list of all variants of this resource
607: contextCall.getClientInfo().getAcceptedMediaTypes().add(
608: new Preference<MediaType>(MediaType.TEXT_URI_LIST));
609: Response contextResponse = getDispatcher().handle(
610: contextCall);
611: if (contextResponse.getEntity() != null) {
612: // Test if the given response is the list of all variants for
613: // this resource
614: if (MediaType.TEXT_URI_LIST.equals(contextResponse
615: .getEntity().getMediaType())) {
616: ReferenceList listVariants = new ReferenceList(
617: contextResponse.getEntity());
618: Set<String> extensions = null;
619: String entryUri;
620: String fullEntryName;
621: String baseEntryName;
622: int lastSlashIndex;
623: int firstDotIndex;
624: for (Reference ref : listVariants) {
625: entryUri = ref.toString();
626: lastSlashIndex = entryUri.lastIndexOf('/');
627: fullEntryName = (lastSlashIndex == -1) ? entryUri
628: : entryUri
629: .substring(lastSlashIndex + 1);
630: baseEntryName = fullEntryName;
631:
632: // Remove the extensions from the base name
633: firstDotIndex = fullEntryName.indexOf('.');
634: if (firstDotIndex != -1) {
635: baseEntryName = fullEntryName.substring(0,
636: firstDotIndex);
637: }
638:
639: // Check if the current file is a valid variant
640: if (baseEntryName.equals(this .baseName)) {
641: boolean validVariant = true;
642:
643: // Verify that the extensions are compatible
644: extensions = getExtensions(fullEntryName);
645: validVariant = (((extensions == null) && (this .baseExtensions == null))
646: || (this .baseExtensions == null) || extensions
647: .containsAll(this .baseExtensions));
648:
649: if (validVariant
650: && (this .baseExtensions != null)
651: && this .baseExtensions
652: .containsAll(extensions)) {
653: // The unique reference has been found.
654: uniqueReference = ref;
655: }
656:
657: if (validVariant) {
658: result.add(ref);
659: }
660: }
661: }
662: } else {
663: result.add(contextResponse.getEntity()
664: .getIdentifier());
665: }
666: }
667: } catch (IOException ioe) {
668: getLogger().log(Level.WARNING,
669: "Unable to get resource variants", ioe);
670: }
671:
672: return result;
673: }
674:
675: /**
676: * Returns the set of extensions contained in a given directory entry name.
677: *
678: * @param entryName
679: * The directory entry name.
680: * @return The set of extensions.
681: */
682: public static Set<String> getExtensions(String entryName) {
683: Set<String> result = new TreeSet<String>();
684: String[] tokens = entryName.split("\\.");
685: for (int i = 1; i < tokens.length; i++) {
686: result.add(tokens[i].toLowerCase());
687: }
688: return result;
689: }
690:
691: /**
692: * Sets the context's target URI (file, clap URI).
693: *
694: * @param targetUri
695: * The context's target URI.
696: */
697: public void setTargetUri(String targetUri) {
698: this.targetUri = targetUri;
699: }
700: }
|