/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.engine.local; import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import org.restlet.Request; import org.restlet.Response; import org.restlet.Restlet; import org.restlet.data.MediaType; import org.restlet.data.Method; import org.restlet.data.Preference; import org.restlet.data.Reference; import org.restlet.data.ReferenceList; import org.restlet.data.Status; import org.restlet.representation.Representation; import org.restlet.representation.Variant; import org.restlet.resource.Directory; import org.restlet.resource.ResourceException; import org.restlet.resource.ServerResource; /** * Resource supported by a set of context representations (from file system, * class loaders and webapp context). A content negotiation mechanism (similar * to Apache HTTP server) is available. It is based on path extensions to detect * variants (languages, media types or character sets). * * @see <a * href="http://httpd.apache.org/docs/2.0/content-negotiation.html">Apache * mod_negotiation module</a> * @author Jerome Louvel * @author Thierry Boileau */ public class DirectoryServerResource extends ServerResource { /** The list of variants for the GET method. */ private volatile List<Variant> variantsGet; /** * The local base name of the resource. For example, "foo.en" and * "foo.en-GB.html" return "foo". */ private volatile String baseName; /** The base variant. */ private volatile Variant baseVariant; /** The parent directory handler. */ private volatile Directory directory; /** If the resource is a directory, this contains its content. */ private volatile ReferenceList directoryContent; /** * If the resource is a directory, the non-trailing slash character leads to * redirection. */ private volatile boolean directoryRedirection; /** Indicates if the target resource is a directory. */ private volatile boolean directoryTarget; /** The context's directory URI (file, clap URI). */ private volatile String directoryUri; /** If the resource is a file, this contains its content. */ private volatile Representation fileContent; /** Indicates if the target resource is a file. */ private volatile boolean fileTarget; /** Indicates if the target resource is a directory with an index. */ private volatile boolean indexTarget; /** The original target URI, in case of extensions tunneling. */ private volatile Reference originalRef; /** The prototype variant. */ private volatile Variant protoVariant; /** The resource path relative to the directory URI. */ private volatile String relativePart; /** The context's target URI (file, clap URI). */ private volatile String targetUri; /** The unique representation of the target URI, if it exists. */ private volatile Reference uniqueReference; @Override public Representation delete() throws ResourceException { if (this.directory.isModifiable()) { Request contextRequest = new Request(Method.DELETE, this.targetUri); Response contextResponse = new Response(contextRequest); if (this.directoryTarget && !this.indexTarget) { contextRequest.setResourceRef(this.targetUri); getClientDispatcher().handle(contextRequest, contextResponse); } else { // Check if there is only one representation // Try to get the unique representation of the resource ReferenceList references = getVariantsReferences(); if (!references.isEmpty()) { if (this.uniqueReference != null) { contextRequest.setResourceRef(this.uniqueReference); getClientDispatcher().handle(contextRequest, contextResponse); } else { // We found variants, but not the right one contextResponse .setStatus(new Status( Status.CLIENT_ERROR_NOT_ACCEPTABLE, "Unable to process properly the request. Several variants exist but none of them suits precisely. ")); } } else { contextResponse.setStatus(Status.CLIENT_ERROR_NOT_FOUND); } } setStatus(contextResponse.getStatus()); } else { setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED, "The directory is not modifiable."); } return null; } /** * This initialization method aims at answering the following questions:<br> * <ul> * <li>does this request target a directory?</li> * <li>does this request target a directory, with an index file?</li> * <li>should this request be redirected (target is a directory with no * trailing "/")?</li> * <li>does this request target a file?</li> * </ul> * <br> * The following constraints must be taken into account:<br> * <ul> * <li>the underlying helper may not support content negotiation and be able * to return the list of possible variants of the target file (e.g. the CLAP * helper).</li> * <li>the underlying helper may not support directory listing</li> * <li>the extensions tunneling cannot apply on a directory</li> * <li>underlying helpers that do not support content negotiation cannot * support extensions tunneling</li> * </ul> */ @Override public void doInit() throws ResourceException { try { // Update the member variables this.directory = (Directory) getRequestAttributes().get( "org.restlet.directory"); this.relativePart = getReference().getRemainingPart(false, false); setNegotiated(this.directory.isNegotiatingContent()); // Restore the original URI in case the call has been tunneled. if ((getApplication() != null) && getApplication().getTunnelService().isExtensionsTunnel()) { this.originalRef = getOriginalRef(); if (this.originalRef != null) { this.originalRef.setBaseRef(getReference().getBaseRef()); this.relativePart = this.originalRef.getRemainingPart(); } } if (this.relativePart.startsWith("/")) { // We enforce the leading slash on the root URI this.relativePart = this.relativePart.substring(1); } // The target URI does not take into account the query and fragment // parts of the resource. this.targetUri = new Reference(directory.getRootRef().toString() + this.relativePart).toString(false, false); preventUpperDirectoryAccess(); if (getClientDispatcher() == null) { getLogger().warning( "No client dispatcher is available on the context. Can't get the target URI: " + this.targetUri); } else { // Try to detect the presence of a directory Response contextResponse = getRepresentation(this.targetUri); if (contextResponse.getEntity() != null) { // As a convention, underlying client connectors return the // directory listing with the media-type // "MediaType.TEXT_URI_LIST" when handling directories if (MediaType.TEXT_URI_LIST.equals(contextResponse .getEntity().getMediaType())) { this.directoryTarget = true; this.fileTarget = false; this.directoryContent = new ReferenceList( contextResponse.getEntity()); if (!getReference().getPath().endsWith("/")) { // All requests will be automatically redirected this.directoryRedirection = true; } if (!this.targetUri.endsWith("/")) { this.targetUri += "/"; this.relativePart += "/"; } // Append the index name if ((getDirectory().getIndexName() != null) && (getDirectory().getIndexName().length() > 0)) { this.directoryUri = this.targetUri; this.baseName = getDirectory().getIndexName(); this.targetUri = this.directoryUri + this.baseName; this.indexTarget = true; } else { this.directoryUri = this.targetUri; this.baseName = null; } } else { // Allows underlying helpers that do not support // "content negotiation" to return the targeted file. // Sometimes we immediately reach the target entity, so // we return it directly. this.directoryTarget = false; this.fileTarget = true; this.fileContent = contextResponse.getEntity(); } } else { this.directoryTarget = false; this.fileTarget = false; // Let's try with the optional index, in case the underlying // client connector does not handle directory listing. if (this.targetUri.endsWith("/")) { // In this case, the trailing "/" shows that the URI // must point to a directory if ((getDirectory().getIndexName() != null) && (getDirectory().getIndexName().length() > 0)) { this.directoryUri = this.targetUri; this.directoryTarget = true; contextResponse = getRepresentation(this.directoryUri + getDirectory().getIndexName()); if (contextResponse.getEntity() != null) { this.baseName = getDirectory().getIndexName(); this.targetUri = this.directoryUri + this.baseName; this.directoryContent = new ReferenceList(); this.directoryContent.add(new Reference( this.targetUri)); this.indexTarget = true; } } } else { // Try to determine if this target URI with no trailing // "/" is a directory, in order to force the // redirection. if ((getDirectory().getIndexName() != null) && (getDirectory().getIndexName().length() > 0)) { // Append the index name contextResponse = getRepresentation(this.targetUri + "/" + getDirectory().getIndexName()); if (contextResponse.getEntity() != null) { this.directoryUri = this.targetUri + "/"; this.baseName = getDirectory().getIndexName(); this.targetUri = this.directoryUri + this.baseName; this.directoryTarget = true; this.directoryRedirection = true; this.directoryContent = new ReferenceList(); this.directoryContent.add(new Reference( this.targetUri)); this.indexTarget = true; } } } } // In case the request does not target a directory and the file // has not been found, try with the tunneled URI. if (isNegotiated() && !this.directoryTarget && !this.fileTarget && (this.originalRef != null)) { this.relativePart = getReference().getRemainingPart(); // The target URI does not take into account the query and // fragment parts of the resource. this.targetUri = new Reference(directory.getRootRef() .toString() + this.relativePart).normalize() .toString(false, false); if (!this.targetUri.startsWith(directory.getRootRef() .toString())) { // Prevent the client from accessing resources in upper // directories this.targetUri = directory.getRootRef().toString(); } } if (!fileTarget || (fileContent == null) || !getRequest().getMethod().isSafe()) { // Try to get the directory content, in case the request // does not target a directory if (!this.directoryTarget) { int lastSlashIndex = this.targetUri.lastIndexOf('/'); if (lastSlashIndex == -1) { this.directoryUri = ""; this.baseName = this.targetUri; } else { this.directoryUri = this.targetUri.substring(0, lastSlashIndex + 1); this.baseName = this.targetUri .substring(lastSlashIndex + 1); } contextResponse = getRepresentation(this.directoryUri); if ((contextResponse.getEntity() != null) && MediaType.TEXT_URI_LIST .equals(contextResponse.getEntity() .getMediaType())) { this.directoryContent = new ReferenceList( contextResponse.getEntity()); } } if (this.baseName != null) { // Analyze extensions this.baseVariant = new Variant(); Entity.updateMetadata(this.baseName, this.baseVariant, true, getMetadataService()); this.protoVariant = new Variant(); Entity.updateMetadata(this.baseName, this.protoVariant, false, getMetadataService()); // Remove stored extensions from the base name this.baseName = Entity.getBaseName(this.baseName, getMetadataService()); } // Check if the resource exists or not. List<Variant> variants = getVariants(Method.GET); if ((variants == null) || (variants.isEmpty())) { setExisting(false); } } // Check if the resource is located in a sub directory. if (isExisting() && !this.directory.isDeeplyAccessible()) { // Count the number of "/" character. int index = this.relativePart.indexOf("/"); if (index != -1) { index = this.relativePart.indexOf("/", index); setExisting((index == -1)); } } } // Log results getLogger().fine("Converted target URI: " + this.targetUri); getLogger().fine("Converted base name : " + this.baseName); } catch (IOException ioe) { throw new ResourceException(ioe); } } /** * Prevent the client from accessing resources in upper directories */ public void preventUpperDirectoryAccess() { String targetUriPath = new Reference(Reference.decode(targetUri)) .normalize().toString(); if (!targetUriPath.startsWith(directory.getRootRef().toString())) { throw new ResourceException(Status.CLIENT_ERROR_FORBIDDEN); } } @Override protected Representation get() throws ResourceException { // Content negotiation has been disabled // The variant that may need to meet the request conditions Representation result = null; List<Variant> variants = getVariants(Method.GET); if ((variants == null) || (variants.isEmpty())) { // Resource not found getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND); } else if (variants.size() == 1) { result = (Representation) variants.get(0); } else { ReferenceList variantRefs = new ReferenceList(); for (Variant variant : variants) { if (variant.getLocationRef() != null) { variantRefs.add(variant.getLocationRef()); } else { getLogger() .warning( "A resource with multiple variants should provide a location for each variant when content negotiation is turned off"); } } if (!variantRefs.isEmpty()) { // Return the list of variants setStatus(Status.REDIRECTION_MULTIPLE_CHOICES); result = variantRefs.getTextRepresentation(); } else { setStatus(Status.CLIENT_ERROR_NOT_FOUND); } } return result; } /** * Returns the local base name of the file. For example, "foo.en" and * "foo.en-GB.html" return "foo". * * @return The local name of the file. */ public String getBaseName() { return this.baseName; } /** * Returns a client dispatcher. * * @return A client dispatcher. */ protected Restlet getClientDispatcher() { return getDirectory().getContext() == null ? null : getDirectory() .getContext().getClientDispatcher(); } /** * Returns the parent directory handler. * * @return The parent directory handler. */ public Directory getDirectory() { return this.directory; } /** * If the resource is a directory, this returns its content. * * @return The directory content. */ protected ReferenceList getDirectoryContent() { return directoryContent; } /** * Returns the context's directory URI (file, clap URI). * * @return The context's directory URI (file, clap URI). */ public String getDirectoryUri() { return this.directoryUri; } /** * Returns a representation of the resource at the target URI. Leverages the * client dispatcher of the parent directory's context. * * @param resourceUri * The URI of the target resource. * @return A response with the representation if success. */ private Response getRepresentation(String resourceUri) { return getClientDispatcher().handle( new Request(Method.GET, resourceUri)); } /** * Returns a representation of the resource at the target URI. Leverages the * client dispatcher of the parent directory's context. * * @param resourceUri * The URI of the target resource. * @param acceptedMediaType * The accepted media type or null. * @return A response with the representation if success. */ protected Response getRepresentation(String resourceUri, MediaType acceptedMediaType) { if (acceptedMediaType == null) { return getClientDispatcher().handle( new Request(Method.GET, resourceUri)); } Request request = new Request(Method.GET, resourceUri); request.getClientInfo().getAcceptedMediaTypes() .add(new Preference<MediaType>(acceptedMediaType)); return getClientDispatcher().handle(request); } /** * Allows to sort the list of representations set by the resource. * * @return A Comparator instance imposing a sort order of representations or * null if no special order is wanted. */ private Comparator<Representation> getRepresentationsComparator() { // Sort the list of representations by their identifier. Comparator<Representation> identifiersComparator = new Comparator<Representation>() { public int compare(Representation rep0, Representation rep1) { boolean bRep0Null = (rep0.getLocationRef() == null); boolean bRep1Null = (rep1.getLocationRef() == null); if (bRep0Null && bRep1Null) { return 0; } if (bRep0Null) { return -1; } if (bRep1Null) { return 1; } return rep0.getLocationRef().getLastSegment() .compareTo(rep1.getLocationRef().getLastSegment()); } }; return identifiersComparator; } /** * Returns the context's target URI (file, clap URI). * * @return The context's target URI (file, clap URI). */ public String getTargetUri() { return this.targetUri; } @Override public List<Variant> getVariants() { return getVariants(getMethod()); } /** * Returns the list of variants for the given method. * * @param method * The related method. * @return The list of variants for the given method. */ @Override protected List<Variant> getVariants(Method method) { List<Variant> result = null; if ((Method.GET.equals(method) || Method.HEAD.equals(method))) { if (variantsGet != null) { result = variantsGet; } else { getLogger().fine("Getting variants for: " + getTargetUri()); if ((this.directoryContent != null) && (getReference() != null) && (getReference().getBaseRef() != null)) { // Allows to sort the list of representations SortedSet<Representation> resultSet = new TreeSet<Representation>( getRepresentationsComparator()); // Compute the base reference (from a call's client point of // view) String baseRef = getReference().getBaseRef().toString( false, false); if (!baseRef.endsWith("/")) { baseRef += "/"; } int lastIndex = this.relativePart.lastIndexOf("/"); if (lastIndex != -1) { baseRef += this.relativePart.substring(0, lastIndex); } int rootLength = getDirectoryUri().length(); if (this.baseName != null) { String filePath; for (Reference ref : getVariantsReferences()) { // Add the new variant to the result list Response contextResponse = getRepresentation(ref .toString()); if (contextResponse.getStatus().isSuccess() && (contextResponse.getEntity() != null)) { filePath = ref.toString(false, false) .substring(rootLength); Representation rep = contextResponse .getEntity(); if (filePath.startsWith("/")) { rep.setLocationRef(baseRef + filePath); } else { rep.setLocationRef(baseRef + "/" + filePath); } resultSet.add(rep); } } } if (!resultSet.isEmpty()) { result = new ArrayList<Variant>(resultSet); } if (resultSet.isEmpty()) { if (this.directoryTarget && getDirectory().isListingAllowed()) { ReferenceList userList = new ReferenceList( this.directoryContent.size()); // Set the list identifier userList.setIdentifier(baseRef); SortedSet<Reference> sortedSet = new TreeSet<Reference>( getDirectory().getComparator()); sortedSet.addAll(this.directoryContent); for (Reference ref : sortedSet) { String filePart = ref.toString(false, false) .substring(rootLength); StringBuilder filePath = new StringBuilder(); if ((!baseRef.endsWith("/")) && (!filePart.startsWith("/"))) { filePath.append('/'); } filePath.append(filePart); userList.add(baseRef + filePath); } List<Variant> list = getDirectory() .getIndexVariants(userList); for (Variant variant : list) { if (result == null) { result = new ArrayList<Variant>(); } result.add(getDirectory() .getIndexRepresentation(variant, userList)); } } } } else if (this.fileTarget && (this.fileContent != null)) { // Sets the location of the target representation. if (getOriginalRef() != null) { this.fileContent.setLocationRef(getRequest() .getOriginalRef()); } else { this.fileContent.setLocationRef(getReference()); } result = new ArrayList<Variant>(); result.add(this.fileContent); } this.variantsGet = result; } } return result; } /** * Returns the references of the representations of the target resource * according to the directory handler property * * @return The list of variants references */ private ReferenceList getVariantsReferences() { ReferenceList result = new ReferenceList(0); try { this.uniqueReference = null; // Ask for the list of all variants of this resource Response contextResponse = getRepresentation(this.targetUri, MediaType.TEXT_URI_LIST); if (contextResponse.getEntity() != null) { // Test if the given response is the list of all variants for // this resource if (MediaType.TEXT_URI_LIST.equals(contextResponse.getEntity() .getMediaType())) { ReferenceList listVariants = new ReferenceList( contextResponse.getEntity()); String entryUri; String fullEntryName; String baseEntryName; int lastSlashIndex; int firstDotIndex; for (Reference ref : listVariants) { entryUri = ref.toString(); lastSlashIndex = entryUri.lastIndexOf('/'); fullEntryName = (lastSlashIndex == -1) ? entryUri : entryUri.substring(lastSlashIndex + 1); baseEntryName = fullEntryName; // Remove the extensions from the base name firstDotIndex = fullEntryName.indexOf('.'); if (firstDotIndex != -1) { baseEntryName = fullEntryName.substring(0, firstDotIndex); } // Check if the current file is a valid variant if (baseEntryName.equals(this.baseName)) { // Test if the variant is included in the base // prototype variant Variant variant = new Variant(); Entity.updateMetadata(fullEntryName, variant, true, getMetadataService()); if (this.protoVariant.includes(variant)) { result.add(ref); } // Test if the variant is equal to the base variant if (this.baseVariant.equals(variant)) { // The unique reference has been found. this.uniqueReference = ref; } } } } else { result.add(contextResponse.getEntity().getLocationRef()); } } } catch (IOException ioe) { getLogger().log(Level.WARNING, "Unable to get resource variants", ioe); } return result; } @Override public Representation handle() { Representation result = null; if (this.directoryRedirection) { if (this.originalRef != null) { if (this.originalRef.hasQuery()) { redirectSeeOther(this.originalRef.getPath() + "/?" + this.originalRef.getQuery()); } else { redirectSeeOther(this.originalRef.getPath() + "/"); } } else { if (getReference().hasQuery()) { redirectSeeOther(getReference().getPath() + "/?" + getReference().getQuery()); } else { redirectSeeOther(getReference().getPath() + "/"); } } } else { result = super.handle(); } return result; } /** * Indicates if the target resource is a directory. * * @return True if the target resource is a directory. */ public boolean isDirectoryTarget() { return this.directoryTarget; } /** * Indicates if the target resource is a file. * * @return True if the target resource is a file. */ public boolean isFileTarget() { return this.fileTarget; } @Override public Representation put(Representation entity) throws ResourceException { if (this.directory.isModifiable()) { // Transfer of PUT calls is only allowed if the readOnly flag is // not set. Request contextRequest = new Request(Method.PUT, this.targetUri); // Add support of partial PUT calls. contextRequest.getRanges().addAll(getRanges()); contextRequest.setEntity(entity); Response contextResponse = new Response(contextRequest); contextRequest.setResourceRef(this.targetUri); getClientDispatcher().handle(contextRequest, contextResponse); setStatus(contextResponse.getStatus()); } else { setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED, "The directory is not modifiable."); } return null; } /** * Sets the context's target URI (file, clap URI). * * @param targetUri * The context's target URI. */ public void setTargetUri(String targetUri) { this.targetUri = targetUri; } }