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.BufferedReader;
022: import java.io.BufferedWriter;
023: import java.io.File;
024: import java.io.FileNotFoundException;
025: import java.io.FileOutputStream;
026: import java.io.FileReader;
027: import java.io.FileWriter;
028: import java.io.IOException;
029: import java.util.ArrayList;
030: import java.util.Iterator;
031: import java.util.List;
032: import java.util.Set;
033: import java.util.TreeSet;
034: import java.util.logging.Level;
035:
036: import org.restlet.Client;
037: import org.restlet.data.Encoding;
038: import org.restlet.data.Language;
039: import org.restlet.data.LocalReference;
040: import org.restlet.data.MediaType;
041: import org.restlet.data.Method;
042: import org.restlet.data.Preference;
043: import org.restlet.data.Protocol;
044: import org.restlet.data.Reference;
045: import org.restlet.data.ReferenceList;
046: import org.restlet.data.Request;
047: import org.restlet.data.Response;
048: import org.restlet.data.Status;
049: import org.restlet.resource.FileRepresentation;
050: import org.restlet.resource.Representation;
051: import org.restlet.resource.Variant;
052: import org.restlet.service.MetadataService;
053: import org.restlet.util.ByteUtils;
054:
055: /**
056: * Connector to the file resources accessible
057: *
058: * @author Jerome Louvel (contact@noelios.com)
059: * @author Thierry Boileau
060: */
061: public class FileClientHelper extends LocalClientHelper {
062: /**
063: * Constructor.
064: *
065: * @param client
066: * The client to help.
067: */
068: public FileClientHelper(Client client) {
069: super (client);
070: getProtocols().add(Protocol.FILE);
071: }
072:
073: /**
074: * Handles a call.
075: *
076: * @param request
077: * The request to handle.
078: * @param response
079: * The response to update.
080: */
081: public void handle(Request request, Response response) {
082: String scheme = request.getResourceRef().getScheme();
083:
084: // Ensure that all ".." and "." are normalized into the path
085: // to preven unauthorized access to user directories.
086: request.getResourceRef().normalize();
087:
088: if (scheme.equalsIgnoreCase("file")) {
089: handleFile(request, response, request.getResourceRef()
090: .getPath());
091: } else {
092: throw new IllegalArgumentException(
093: "Protocol \""
094: + scheme
095: + "\" not supported by the connector. Only FILE is supported.");
096: }
097: }
098:
099: /**
100: * Handles a call for the FILE protocol.
101: *
102: * @param request
103: * The request to handle.
104: * @param response
105: * The response to update.
106: * @param path
107: * The file or directory path.
108: */
109: protected void handleFile(Request request, Response response,
110: String path) {
111: // As the path may be percent-encoded, it has to be percent-decoded.
112: // Then, all generated uris must be encoded.
113: String decodedPath = LocalReference.localizePath(Reference
114: .decode(path));
115: File file = new File(decodedPath);
116: MetadataService metadataService = getMetadataService(request);
117:
118: if (request.getMethod().equals(Method.GET)
119: || request.getMethod().equals(Method.HEAD)) {
120: Representation output = null;
121:
122: // Get variants for a resource
123: boolean found = false;
124: Iterator<Preference<MediaType>> iterator = request
125: .getClientInfo().getAcceptedMediaTypes().iterator();
126: while (iterator.hasNext() && !found) {
127: Preference<MediaType> pref = iterator.next();
128: found = pref.getMetadata().equals(
129: MediaType.TEXT_URI_LIST);
130: }
131: if (found) {
132: // Try to list all variants of this resource
133: // 1- set up base name as the longest part of the name without
134: // known extensions (beginning from the left)
135: String baseName = getBaseName(file, metadataService);
136: // 2- looking for resources with the same base name
137: if (file.getParentFile() != null) {
138: File[] files = file.getParentFile().listFiles();
139: if (files != null) {
140: ReferenceList rl = new ReferenceList(
141: files.length);
142:
143: String encodedParentDirectoryURI = path
144: .substring(0, path.lastIndexOf("/"));
145: String encodedFileName = path.substring(path
146: .lastIndexOf("/") + 1);
147:
148: for (File entry : files) {
149: if (baseName.equals(getBaseName(entry,
150: metadataService))) {
151: rl
152: .add(LocalReference
153: .createFileReference(encodedParentDirectoryURI
154: + "/"
155: + getReencodedVariantFileName(
156: encodedFileName,
157: entry
158: .getName())));
159: }
160: }
161: output = rl.getTextRepresentation();
162: }
163: }
164: } else {
165: if ((file != null) && file.exists()) {
166: if (file.isDirectory()) {
167: // Return the directory listing
168: File[] files = file.listFiles();
169: ReferenceList rl = new ReferenceList(
170: files.length);
171: rl.setIdentifier(request.getResourceRef());
172: String directoryUri = request.getResourceRef()
173: .toString();
174:
175: // Ensures that the directory URI ends with a slash
176: if (!directoryUri.endsWith("/")) {
177: directoryUri += "/";
178: }
179:
180: for (File entry : files) {
181: rl.add(directoryUri + entry.getName());
182: }
183:
184: output = rl.getTextRepresentation();
185: } else {
186: // Return the file content
187: output = new FileRepresentation(file,
188: metadataService.getDefaultMediaType(),
189: getTimeToLive());
190: updateMetadata(metadataService, file.getName(),
191: output);
192: }
193: }
194: }
195:
196: if (output == null) {
197: response.setStatus(Status.CLIENT_ERROR_NOT_FOUND);
198: } else {
199: output.setIdentifier(request.getResourceRef());
200: response.setEntity(output);
201: response.setStatus(Status.SUCCESS_OK);
202: }
203: } else if (request.getMethod().equals(Method.PUT)) {
204: // Several checks : first the consistency of the metadata and the
205: // filename
206: if (!checkMetadataConsistency(file.getName(),
207: metadataService, request.getEntity())) {
208: // ask the client to reiterate properly its request
209: response
210: .setStatus(new Status(
211: Status.REDIRECTION_SEE_OTHER,
212: "The metadata are not consistent with the URI"));
213: } else {
214: // Deals with directory
215: boolean isDirectory = false;
216: if (file.exists()) {
217: if (file.isDirectory()) {
218: isDirectory = true;
219: response
220: .setStatus(new Status(
221: Status.CLIENT_ERROR_FORBIDDEN,
222: "Can't put a new representation of a directory"));
223: }
224: } else {
225: // No existing file or directory found
226: if (path.endsWith("/")) {
227: isDirectory = true;
228: // Create a new directory and its necessary parents
229: if (file.mkdirs()) {
230: response
231: .setStatus(Status.SUCCESS_NO_CONTENT);
232: } else {
233: getLogger()
234: .log(Level.WARNING,
235: "Unable to create the new directory");
236: response
237: .setStatus(new Status(
238: Status.SERVER_ERROR_INTERNAL,
239: "Unable to create the new directory"));
240: }
241: }
242: }
243:
244: if (!isDirectory) {
245: // We look for the possible variants
246: // 1- set up base name as the longest part of the name
247: // without known extensions (beginning from the left)
248: String baseName = getBaseName(file, metadataService);
249: Set<String> extensions = getExtensions(file,
250: metadataService);
251: // 2- loooking for resources with the same base name
252: File[] files = file.getParentFile().listFiles();
253: File uniqueVariant = null;
254:
255: List<File> variantsList = new ArrayList<File>();
256: if (files != null) {
257: for (File entry : files) {
258: if (entry.getName().startsWith(baseName)) {
259: Set<String> entryExtensions = getExtensions(
260: entry, metadataService);
261: if (entryExtensions
262: .containsAll(extensions)) {
263: variantsList.add(entry);
264: if (extensions
265: .containsAll(entryExtensions)) {
266: // The right representation has been
267: // found.
268: uniqueVariant = entry;
269: }
270: }
271: }
272: }
273: }
274: if (uniqueVariant != null) {
275: file = uniqueVariant;
276: } else {
277: if (!variantsList.isEmpty()) {
278: // Negociated resource (several variants, but not
279: // the right one).
280: // Check if the request could be completed or not.
281: // The request could be more precise
282: response
283: .setStatus(new Status(
284: Status.CLIENT_ERROR_NOT_ACCEPTABLE,
285: "Unable to process properly the request. Several variants exist but none of them suits precisely."));
286: } else {
287: // This resource does not exist, yet.
288: // Complete it with the default metadata
289: updateMetadata(metadataService, file
290: .getName(), request.getEntity());
291: if (request.getEntity().getLanguages()
292: .isEmpty()) {
293: if (metadataService
294: .getDefaultLanguage() != null) {
295: request
296: .getEntity()
297: .getLanguages()
298: .add(
299: metadataService
300: .getDefaultLanguage());
301: }
302: }
303: if (request.getEntity().getMediaType() == null) {
304: request.getEntity().setMediaType(
305: metadataService
306: .getDefaultMediaType());
307: }
308: if (request.getEntity().getEncodings()
309: .isEmpty()) {
310: if (metadataService
311: .getDefaultEncoding() != null
312: && !metadataService
313: .getDefaultEncoding()
314: .equals(
315: Encoding.IDENTITY)) {
316: request
317: .getEntity()
318: .getEncodings()
319: .add(
320: metadataService
321: .getDefaultEncoding());
322: }
323: }
324: // Update the URI
325: StringBuilder fileName = new StringBuilder(
326: baseName);
327: if (metadataService.getExtension(request
328: .getEntity().getMediaType()) != null) {
329: fileName
330: .append("."
331: + metadataService
332: .getExtension(request
333: .getEntity()
334: .getMediaType()));
335: }
336: for (Language language : request
337: .getEntity().getLanguages()) {
338: if (metadataService
339: .getExtension(language) != null) {
340: fileName
341: .append("."
342: + metadataService
343: .getExtension(language));
344: }
345: }
346: for (Encoding encoding : request
347: .getEntity().getEncodings()) {
348: if (metadataService
349: .getExtension(encoding) != null) {
350: fileName
351: .append("."
352: + metadataService
353: .getExtension(encoding));
354: }
355: }
356: file = new File(file.getParentFile(),
357: fileName.toString());
358: }
359: }
360: // Before putting the file representation, we check that all
361: // the extensions are known
362: if (!checkExtensionsConsistency(file,
363: metadataService)) {
364: response
365: .setStatus(new Status(
366: Status.SERVER_ERROR_INTERNAL,
367: "Unable to process properly the URI. At least one extension is not known by the server."));
368: } else {
369: File tmp = null;
370:
371: if (file.exists()) {
372: FileOutputStream fos = null;
373: // Replace the content of the file
374: // First, create a temporary file
375: try {
376: tmp = File.createTempFile(
377: "restlet-upload", "bin");
378:
379: if (request.isEntityAvailable()) {
380: fos = new FileOutputStream(tmp);
381: ByteUtils.write(request.getEntity()
382: .getStream(), fos);
383: }
384: } catch (IOException ioe) {
385: getLogger()
386: .log(
387: Level.WARNING,
388: "Unable to create the temporary file",
389: ioe);
390: response
391: .setStatus(new Status(
392: Status.SERVER_ERROR_INTERNAL,
393: "Unable to create a temporary file"));
394: } finally {
395: try {
396: if (fos != null)
397: fos.close();
398: } catch (IOException ioe) {
399: getLogger()
400: .log(
401: Level.WARNING,
402: "Unable to close the temporary file",
403: ioe);
404: response
405: .setStatus(new Status(
406: Status.SERVER_ERROR_INTERNAL,
407: "Unable to close a temporary file"));
408: }
409: }
410:
411: // Then delete the existing file
412: if (file.delete()) {
413: // Finally move the temporary file to the
414: // existing file location
415: boolean renameSuccessfull = false;
416: if ((tmp != null) && tmp.renameTo(file)) {
417: if (request.getEntity() == null) {
418: response
419: .setStatus(Status.SUCCESS_NO_CONTENT);
420: } else {
421: response
422: .setStatus(Status.SUCCESS_OK);
423: }
424: renameSuccessfull = true;
425: } else {
426: // Many aspects of the behavior of the
427: // method "renameTo" are inherently
428: // platform-dependent: The rename operation
429: // might not be able to move a file from one
430: // filesystem to another.
431: if (tmp != null && tmp.exists()) {
432: try {
433: BufferedReader br = new BufferedReader(
434: new FileReader(tmp));
435: BufferedWriter wr = new BufferedWriter(
436: new FileWriter(file));
437: String s;
438: while ((s = br.readLine()) != null)
439: wr.append(s);
440:
441: br.close();
442: wr.flush();
443: wr.close();
444: renameSuccessfull = true;
445: tmp.delete();
446: } catch (Exception e) {
447: renameSuccessfull = false;
448: }
449: }
450: if (!renameSuccessfull) {
451: getLogger()
452: .log(Level.WARNING,
453: "Unable to move the temporary file to replace the existing file");
454: response
455: .setStatus(new Status(
456: Status.SERVER_ERROR_INTERNAL,
457: "Unable to move the temporary file to replace the existing file"));
458: }
459: }
460: } else {
461: getLogger()
462: .log(Level.WARNING,
463: "Unable to delete the existing file");
464: response
465: .setStatus(new Status(
466: Status.SERVER_ERROR_INTERNAL,
467: "Unable to delete the existing file"));
468: }
469: } else {
470: File parent = file.getParentFile();
471: if ((parent != null) && !parent.exists()) {
472: // Create the parent directories then the new
473: // file
474: if (!parent.mkdirs()) {
475: getLogger()
476: .log(Level.WARNING,
477: "Unable to create the parent directory");
478: response
479: .setStatus(new Status(
480: Status.SERVER_ERROR_INTERNAL,
481: "Unable to create the parent directory"));
482: }
483: }
484: FileOutputStream fos = null;
485: // Create the new file
486: try {
487: if (file.createNewFile()) {
488: if (request.getEntity() == null) {
489: response
490: .setStatus(Status.SUCCESS_NO_CONTENT);
491: } else {
492: fos = new FileOutputStream(file);
493: ByteUtils.write(request
494: .getEntity()
495: .getStream(), fos);
496: response
497: .setStatus(Status.SUCCESS_CREATED);
498: }
499: } else {
500: getLogger()
501: .log(Level.WARNING,
502: "Unable to create the new file");
503: response
504: .setStatus(new Status(
505: Status.SERVER_ERROR_INTERNAL,
506: "Unable to create the new file"));
507: }
508: } catch (FileNotFoundException fnfe) {
509: getLogger()
510: .log(
511: Level.WARNING,
512: "Unable to create the new file",
513: fnfe);
514: response
515: .setStatus(new Status(
516: Status.SERVER_ERROR_INTERNAL,
517: "Unable to create the new file"));
518: } catch (IOException ioe) {
519: getLogger()
520: .log(
521: Level.WARNING,
522: "Unable to create the new file",
523: ioe);
524: response
525: .setStatus(new Status(
526: Status.SERVER_ERROR_INTERNAL,
527: "Unable to create the new file"));
528: } finally {
529: try {
530: if (fos != null)
531: fos.close();
532: } catch (IOException ioe) {
533: getLogger()
534: .log(
535: Level.WARNING,
536: "Unable to close the temporary file",
537: ioe);
538: response
539: .setStatus(new Status(
540: Status.SERVER_ERROR_INTERNAL,
541: "Unable to close a temporary file"));
542: }
543: }
544: }
545: }
546: }
547: }
548: } else if (request.getMethod().equals(Method.DELETE)) {
549: if (file.isDirectory()) {
550: if (file.listFiles().length == 0) {
551: if (file.delete()) {
552: response.setStatus(Status.SUCCESS_NO_CONTENT);
553: } else {
554: response.setStatus(new Status(
555: Status.SERVER_ERROR_INTERNAL,
556: "Couldn't delete the directory"));
557: }
558: } else {
559: response.setStatus(new Status(
560: Status.CLIENT_ERROR_FORBIDDEN,
561: "Couldn't delete the non-empty directory"));
562: }
563: } else {
564: if (file.delete()) {
565: response.setStatus(Status.SUCCESS_NO_CONTENT);
566: } else {
567: response.setStatus(new Status(
568: Status.SERVER_ERROR_INTERNAL,
569: "Couldn't delete the file"));
570: }
571: }
572: } else {
573: response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
574: response.getAllowedMethods().add(Method.GET);
575: response.getAllowedMethods().add(Method.HEAD);
576: response.getAllowedMethods().add(Method.PUT);
577: response.getAllowedMethods().add(Method.DELETE);
578: }
579: }
580:
581: /**
582: * Returns the base name as the longest part of the name without known
583: * extensions (beginning from the left)
584: *
585: * @param file
586: * @param metadataService
587: * @return the base name of the file
588: */
589: private String getBaseName(File file,
590: MetadataService metadataService) {
591: String[] result = file.getName().split("\\.");
592: StringBuilder baseName = new StringBuilder().append(result[0]);
593: boolean extensionFound = false;
594: for (int i = 1; (i < result.length) && !extensionFound; i++) {
595: extensionFound = metadataService.getMetadata(result[i]) != null;
596: if (!extensionFound) {
597: baseName.append(".").append(result[i]);
598: }
599: }
600: return baseName.toString();
601: }
602:
603: /**
604: * Returns the Set of extensions of a file
605: *
606: * @param file
607: * @param metadataService
608: * @return
609: */
610: private Set<String> getExtensions(File file,
611: MetadataService metadataService) {
612: Set<String> result = new TreeSet<String>();
613:
614: String[] tokens = file.getName().split("\\.");
615: boolean extensionFound = false;
616: int i;
617: for (i = 1; (i < tokens.length) && !extensionFound; i++) {
618: extensionFound = metadataService.getMetadata(tokens[i]) != null;
619: }
620: if (extensionFound) {
621: for (--i; (i < tokens.length); i++) {
622: result.add(tokens[i]);
623: }
624: }
625:
626: return result;
627: }
628:
629: /**
630: * Checks that the URI and the representation are compatible. The whole set
631: * of metadata of the representation must be included in the set of those of
632: * the URI
633: *
634: * @param fileName
635: * The name of the resource
636: * @param metadataService
637: * metadata helper
638: * @param representation
639: * the provided representation
640: * @return true if the metadata of the representation are compatible with
641: * the metadata extracted from the filename
642: */
643: private boolean checkMetadataConsistency(String fileName,
644: MetadataService metadataService,
645: Representation representation) {
646: boolean result = true;
647: if (representation != null) {
648: Variant var = new Variant();
649: updateMetadata(metadataService, fileName, var);
650: // "rep" contains the theorical correct metadata
651: if (!representation.getLanguages().isEmpty()
652: && !var.getLanguages().containsAll(
653: representation.getLanguages())) {
654: result = false;
655: }
656: if (representation.getMediaType() != null
657: && !(var.getMediaType() != null && var
658: .getMediaType().includes(
659: representation.getMediaType()))) {
660: result = false;
661: }
662: if (!representation.getEncodings().isEmpty()
663: && !var.getEncodings().containsAll(
664: representation.getEncodings())) {
665: result = false;
666: }
667: }
668: return result;
669: }
670:
671: /**
672: * Check that all extensions of the file correspond to a known metadata
673: *
674: * @param file
675: * @param metadataService
676: * @param representation
677: * @return
678: */
679: private boolean checkExtensionsConsistency(File file,
680: MetadataService metadataService) {
681: boolean knownExtension = true;
682:
683: Set<String> set = getExtensions(file, metadataService);
684: Iterator<String> iterator = set.iterator();
685: while (iterator.hasNext() && knownExtension) {
686: knownExtension = metadataService.getMetadata(iterator
687: .next()) != null;
688: }
689:
690: return knownExtension;
691: }
692:
693: /**
694: * Percent-encodes the given percent-decoded variant name of a resource
695: * whose percent-encoded name is given. Tries to match the longest common
696: * part of both encoded file name and decoded variant name.
697: *
698: * @param encodedFileName
699: * the percent-encoded name of the initial resource
700: * @param decodedVariantFileName
701: * the percent-decoded file name of a variant of the initial
702: * resource.
703: * @return the variant percent-encoded file name.
704: */
705: private String getReencodedVariantFileName(String encodedFileName,
706: String decodedVariantFileName) {
707: int i = 0;
708: int j = 0;
709: boolean stop = false;
710: for (i = 0; i < decodedVariantFileName.length()
711: && (j < encodedFileName.length()) && !stop; i++) {
712: String decodedChar = decodedVariantFileName.substring(i,
713: i + 1);
714: if (decodedChar.equals(encodedFileName.substring(j, j + 1))) {
715: j++;
716: } else {
717: if (encodedFileName.substring(j, j + 1).equals("%")) {
718: if (decodedChar
719: .equals(Reference.decode(encodedFileName
720: .substring(j, j + 3)))) {
721: j += 3;
722: } else {
723: stop = true;
724: }
725: } else {
726: if (decodedChar
727: .equals(Reference.decode(encodedFileName
728: .substring(j, j + 1)))) {
729: j++;
730: } else {
731: stop = true;
732: }
733: }
734: }
735: }
736:
737: if (stop) {
738: return encodedFileName.substring(0, j)
739: + decodedVariantFileName.substring(i - 1);
740: } else {
741: if (j == encodedFileName.length()) {
742: return encodedFileName.substring(0, j)
743: + decodedVariantFileName.substring(i);
744: } else {
745: return encodedFileName.substring(0, j);
746: }
747: }
748: }
749: }
|