package er.attachment.processors; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.UUID; import org.apache.log4j.Logger; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WORequest; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.foundation.NSMutableDictionary; import er.attachment.ERAttachmentRequestHandler; import er.attachment.model.ERAttachment; import er.attachment.model.ERCloudFilesAttachment; import er.attachment.model.ERDatabaseAttachment; import er.attachment.model.ERFileAttachment; import er.attachment.model.ERPendingAttachment; import er.attachment.model.ERS3Attachment; import er.attachment.thumbnail.ERThumbnailer; import er.attachment.thumbnail.IERThumbnailer; import er.attachment.utils.ERMimeType; import er.attachment.utils.ERMimeTypeManager; import er.extensions.crypting.ERXCrypto; import er.extensions.foundation.ERXFileUtilities; import er.extensions.foundation.ERXProperties; import er.extensions.validation.ERXValidationException; /** * <p> * ERAttachmentProcessors provide the implementation of the communication with the * attachment storage method, including import, URL generation, and stream * generation. * </p> * * <p> * ERAttachmentProcessors also provide support for path template variables. Read * the er.attachment package.html for more information. * </p> * * @param <T> the type of ERAttachment that this processor processes * * @property er.attachment.maxSize the maximum size of an uploaded attachment * @property er.attachment.[configurationName].maxSize the maximum size of an uploaded attachment * @property er.attachment.[configurationName].storageType * @property er.attachment.storageType * @property er.attachment.[configurationName].proxyAsAttachment * @property er.attachment.proxyAsAttachment * * @author mschrag */ public abstract class ERAttachmentProcessor<T extends ERAttachment> { public static final Logger log = Logger.getLogger(ERAttachmentProcessor.class); private static final String EXT_VARIABLE = "\\$\\{ext\\}"; private static final String HASH_VARIABLE = "\\$\\{hash\\}"; private static final String PK_VARIABLE = "\\$\\{pk\\}"; private static final String FILE_NAME_VARIABLE = "\\$\\{fileName\\}"; private static final String UUID_VARIABLE = "\\$\\{uuid\\}"; private static NSMutableDictionary<String, ERAttachmentProcessor<?>> _processors; private IERAttachmentProcessorDelegate _delegate; /** * Returns all of the processors mapped by storageType. * * @return all of the processors mapped by storageType */ public static synchronized NSMutableDictionary<String, ERAttachmentProcessor<?>> processors() { if (_processors == null) { _processors = new NSMutableDictionary<String, ERAttachmentProcessor<?>>(); _processors.setObjectForKey(new ERDatabaseAttachmentProcessor(), ERDatabaseAttachment.STORAGE_TYPE); _processors.setObjectForKey(new ERS3AttachmentProcessor(), ERS3Attachment.STORAGE_TYPE); _processors.setObjectForKey(new ERFileAttachmentProcessor(), ERFileAttachment.STORAGE_TYPE); _processors.setObjectForKey(new ERCloudFilesAttachmentProcessor(), ERCloudFilesAttachment.STORAGE_TYPE); } return _processors; } /** * Returns the processor that corresponds to the given attachment. * * @param <T> the attachment type * @param attachment the attachment to lookup a processor for * @return the attachment's processor */ public static <T extends ERAttachment> ERAttachmentProcessor<T> processorForType(T attachment) { return ERAttachmentProcessor.processorForType(attachment == null ? null : attachment.storageType()); } /** * Returns the processor that corresponds to the given storage type ("s3", "db", "file", etc). * * @param <T> the attachment type * @param storageType the type of processor to lookup * @return the storage type's processor */ @SuppressWarnings("unchecked") public static <T extends ERAttachment> ERAttachmentProcessor<T> processorForType(String storageType) { ERAttachmentProcessor<T> processor = (ERAttachmentProcessor<T>) ERAttachmentProcessor.processors().objectForKey(storageType); if (processor == null) { throw new IllegalArgumentException("There is no attachment processor for the type '" + storageType + "'."); } return processor; } /** * Returns the processor that corresponds to the given configuration name ("s3", "db", "file", etc). * * @param <T> the attachment type * @param configurationName the configuration name to use to lookup the default storage type * @return the storage type's processor */ public static <T extends ERAttachment> ERAttachmentProcessor<T> processorForConfigurationName(String configurationName) { String storageType = ERXProperties.stringForKey("er.attachment." + configurationName + ".storageType"); if (storageType == null) { storageType = ERXProperties.stringForKeyWithDefault("er.attachment.storageType", ERDatabaseAttachment.STORAGE_TYPE); } return processorForType(storageType); } /** * Adds a new attachment processor for the given storage type. * * @param processor the processor * @param storageType the storage type that corresponds to the processor */ public static synchronized void addAttachmentProcessorForType(ERAttachmentProcessor<?> processor, String storageType) { ERAttachmentProcessor.processors().setObjectForKey(processor, storageType); } /** * Parses a path template with ${ext}, ${fileName}, ${hash}, ${uuid}, and ${pk} variables in it. See the ERAttachment * top level documentation for more information. * * @param attachment the attachment being processed * @param templatePath the template path definition * @param recommendedFileName the original file name recommended by the uploading user * @return compiled path */ protected static String _parsePathTemplate(ERAttachment attachment, String templatePath, String recommendedFileName) { String parsedPath = templatePath; String ext = ERMimeTypeManager.primaryExtension(attachment.mimeType()); if (ext == null) { ext = ERXFileUtilities.fileExtension(recommendedFileName); } if (ext != null) { parsedPath = parsedPath.replaceAll(ERAttachmentProcessor.EXT_VARIABLE, "." + ext); } else { parsedPath = parsedPath.replaceAll(ERAttachmentProcessor.EXT_VARIABLE, ""); } String filenameHash = ERXCrypto.shaEncode(recommendedFileName); StringBuilder hashPathBuffer = new StringBuilder(); hashPathBuffer.append(filenameHash.charAt(0)); hashPathBuffer.append('/'); hashPathBuffer.append(filenameHash.charAt(1)); hashPathBuffer.append('/'); hashPathBuffer.append(filenameHash.charAt(2)); //hashPathBuffer.append('/'); //hashPathBuffer.append(filenameHash.substring(3)); parsedPath = parsedPath.replaceAll(ERAttachmentProcessor.HASH_VARIABLE, hashPathBuffer.toString()); parsedPath = parsedPath.replaceAll(ERAttachmentProcessor.UUID_VARIABLE, UUID.randomUUID().toString()); parsedPath = parsedPath.replaceAll(ERAttachmentProcessor.FILE_NAME_VARIABLE, recommendedFileName); parsedPath = parsedPath.replaceAll(ERAttachmentProcessor.PK_VARIABLE, attachment.primaryKeyInTransaction()); return parsedPath; } /** * Sets the attachment processor delegate for this processor. * * @param delegate the attachment processor delegate for this processor */ public void setDelegate(IERAttachmentProcessorDelegate delegate) { _delegate = delegate; } /** * Returns the attachment processor delegate for this processor. * * @return the attachment processor delegate for this processor */ public IERAttachmentProcessorDelegate delegate() { return _delegate; } /** * Returns a URL to the given attachment that routes via the ERAttachmentRequestHandler. * * @param attachment the attachment to proxy * @param context the context * @return an ERAttachmentRequestHandler URL */ protected String proxiedUrl(T attachment, WOContext context) { String webPath = attachment.webPath(); if (webPath.startsWith("/")) { webPath = webPath.substring(1); } webPath = "id/" + attachment.primaryKey() + "/" + webPath; String attachmentUrl = context.urlWithRequestHandlerKey(ERAttachmentRequestHandler.REQUEST_HANDLER_KEY, webPath, null); return attachmentUrl; } /** * Processes an uploaded file, imports it into the appropriate data store, and returns an ERAttachment that * represents it. uploadedFile will NOT be deleted after the import process is complete. * * @param editingContext the EOEditingContext to create the ERAttachment in * @param uploadedFile the file to attach (which will NOT be deleted at the end) * @return an ERAttachment that represents the file * @throws IOException if the processing fails */ public T process(EOEditingContext editingContext, File uploadedFile) throws IOException { ERPendingAttachment pendingAttachment = new ERPendingAttachment(uploadedFile, uploadedFile.getName(), null, null, null); pendingAttachment.setPendingDelete(false); return process(editingContext, pendingAttachment); } /** * Processes an uploaded file, imports it into the appropriate data store, and returns an ERAttachment that * represents it. uploadedFile will be deleted after the import process is complete. * * @param editingContext the EOEditingContext to create the ERAttachment in * @param uploadedFile the uploaded temporary file (which will be deleted at the end) * @param recommendedFilePath the filename recommended by the user during import * @return an ERAttachment that represents the file * @throws IOException if the processing fails */ public T process(EOEditingContext editingContext, File uploadedFile, String recommendedFilePath) throws IOException { return process(editingContext, new ERPendingAttachment(uploadedFile, recommendedFilePath, null, null, null)); } /** * Processes an uploaded file, imports it into the appropriate data store, and returns an ERAttachment that * represents it. uploadedFile will be deleted after the import process is complete. * * @param editingContext the EOEditingContext to create the ERAttachment in * @param uploadedFile the uploaded temporary file (which will be deleted at the end) * @param recommendedFilePath the filename recommended by the user during import * @param mimeType the mimeType to use (null = guess based on file extension) * @return an ERAttachment that represents the file * @throws IOException if the processing fails */ public T process(EOEditingContext editingContext, File uploadedFile, String recommendedFilePath, String mimeType) throws IOException { return process(editingContext, new ERPendingAttachment(uploadedFile, recommendedFilePath, mimeType, null, null)); } /** * Processes an uploaded file, imports it into the appropriate data store, and returns an ERAttachment that * represents it. uploadedFile will be deleted after the import process is complete. * * @param editingContext the EOEditingContext to create the ERAttachment in * @param uploadedFile the uploaded temporary file (which will be deleted at the end) * @param recommendedFilePath the filename recommended by the user during import * @param mimeType the mimeType to use (null = guess based on file extension) * @param configurationName the name of the configuration settings to use for this processor (see top level docs) * @param ownerID an arbitrary string that represents the ID of the "owner" of this thumbnail (Person.primaryKey, for instance) * @return an ERAttachment that represents the file * @throws IOException if the processing fails */ public T process(EOEditingContext editingContext, File uploadedFile, String recommendedFilePath, String mimeType, String configurationName, String ownerID) throws IOException { return process(editingContext, new ERPendingAttachment(uploadedFile, recommendedFilePath, mimeType, configurationName, ownerID)); } /** * Processes an uploaded file, imports it into the appropriate data store, and returns an ERAttachment that * represents it. uploadedFile will be deleted after the import process is complete. * * @param editingContext the EOEditingContext to create the ERAttachment in * @param uploadedFile the uploaded temporary file (which will be deleted at the end) * @param recommendedFilePath the filename recommended by the user during import * @param mimeType the mimeType to use (null = guess based on file extension) * @param configurationName the name of the configuration settings to use for this processor (see top level docs) * @param ownerID an arbitrary string that represents the ID of the "owner" of this thumbnail (Person.primaryKey, for instance) * @param width the desired width of the attachment * @param height the desired height of the attachment * @return an ERAttachment that represents the file * @throws IOException if the processing fails */ public T process(EOEditingContext editingContext, File uploadedFile, String recommendedFilePath, String mimeType, int width, int height, String configurationName, String ownerID) throws IOException { return process(editingContext, new ERPendingAttachment(uploadedFile, recommendedFilePath, mimeType, width, height, configurationName, ownerID)); } /** * Processes an uploaded file, imports it into the appropriate data store, and returns an ERAttachment that * represents it. uploadedFile will be deleted after the import process is complete. * * @param editingContext the EOEditingContext to create the ERAttachment in * @param pendingAttachment the ERPendingAttachment that encapsulates the import information * @return an ERAttachment that represents the file * @throws IOException if the processing fails */ public T process(EOEditingContext editingContext, ERPendingAttachment pendingAttachment) throws IOException { File uploadedFile = pendingAttachment.uploadedFile(); String recommendedFileName = pendingAttachment.recommendedFileName(); String configurationName = pendingAttachment.configurationName(); long maxSize = ERXProperties.longForKey("er.attachment." + configurationName + ".maxSize"); if (maxSize == 0) { maxSize = ERXProperties.longForKeyWithDefault("er.attachment.maxSize", 0); } if (maxSize > 0 && uploadedFile.length() > maxSize) { if (pendingAttachment.isPendingDelete()) { uploadedFile.delete(); } ERXAttachmentExceedsLengthException maxSizeExceededException = new ERXAttachmentExceedsLengthException("AttachmentExceedsMaximumLengthException", uploadedFile, "size", maxSize, recommendedFileName); throw maxSizeExceededException; } String suggestedMimeType = pendingAttachment.mimeType(); if (suggestedMimeType != null) { ERMimeType erMimeType = ERMimeTypeManager.mimeTypeManager().mimeTypeForMimeTypeString(suggestedMimeType, false); if (erMimeType == null) { suggestedMimeType = null; } } if (suggestedMimeType == null) { String extension = ERXFileUtilities.fileExtension(recommendedFileName); ERMimeType erMimeType = ERMimeTypeManager.mimeTypeManager().mimeTypeForExtension(extension, false); if (erMimeType != null) { suggestedMimeType = erMimeType.mimeType(); } if (suggestedMimeType == null) { suggestedMimeType = "application/x-octet-stream"; } } int width = pendingAttachment.width(); int height = pendingAttachment.height(); if (width != -1 || height != -1) { ERMimeType mimeType = ERMimeTypeManager.mimeTypeManager().mimeTypeForMimeTypeString(suggestedMimeType, false); if (mimeType != null) { IERThumbnailer thumbnailer = ERThumbnailer.thumbnailer(mimeType); if (thumbnailer != null) { thumbnailer.thumbnail(width, height, uploadedFile, uploadedFile, mimeType); } } } String ownerID = pendingAttachment.ownerID(); T attachment = _process(editingContext, uploadedFile, recommendedFileName, suggestedMimeType, configurationName, ownerID, pendingAttachment.isPendingDelete()); attachment.setConfigurationName(configurationName); attachment.setOwnerID(ownerID); return attachment; } /** * Called after an attachment has been inserted (from didInsert). * * @param attachment the inserted attachment */ public void attachmentInserted(T attachment) { // DO NOTHING BY DEFAULT } /** * Returns whether or not the proxy request handler should return this as an attachment * with a Content-Disposition. * * @return true if the proxy should use a content-disposition */ public boolean proxyAsAttachment(T attachment) { boolean proxyAsAttachment = false; String proxyAsAttachmentStr = ERXProperties.stringForKey("er.attachment." + attachment.configurationName() + ".proxyAsAttachment"); if (proxyAsAttachmentStr == null) { proxyAsAttachmentStr = ERXProperties.stringForKey("er.attachment.proxyAsAttachment"); } if (proxyAsAttachmentStr != null) { proxyAsAttachment = Boolean.parseBoolean(proxyAsAttachmentStr); } return proxyAsAttachment; } /** * Processes an uploaded file, imports it into the appropriate data store, and returns an ERAttachment that * represents it. uploadedFile will be deleted after the import process is complete. * * @param editingContext the EOEditingContext to create the ERAttachment in * @param uploadedFile the uploaded temporary file (which will be deleted at the end) * @param recommendedFileName the filename recommended by the user during import * @param mimeType the mimeType to use (null = guess based on file extension) * @param configurationName the name of the configuration settings to use for this processor (see top level docs) * @param ownerID an arbitrary string that represents the ID of the "owner" of this thumbnail (Person.primaryKey, for instance) * @param pendingDelete if true, the uploadedFile will be deleted after import; if false, it will be left alone * @return an ERAttachment that represents the file * @throws IOException if the processing fails */ public abstract T _process(EOEditingContext editingContext, File uploadedFile, String recommendedFileName, String mimeType, String configurationName, String ownerID, boolean pendingDelete) throws IOException; /** * Returns an InputStream to the data of the given attachment. * * @param attachment the attachment to retrieve the data for * @return an InputStream onto the data * @throws IOException if the stream cannot be created */ public abstract InputStream attachmentInputStream(T attachment) throws IOException; /** * Returns a URL to the attachment's data. * * @param attachment the attachment to generate a URL for * @param request the current request * @param context the current context * * @return a URL to the attachment's data */ public abstract String attachmentUrl(T attachment, WORequest request, WOContext context); /** * Deletes the attachment from the data store. * * @param attachment the attachment to delete * @throws IOException if the delete fails */ public abstract void deleteAttachment(T attachment) throws IOException; /** * ERXAttachmentExceedsLengthException thrown when an attachment exceeds the maximum attachment size. * * @author mschrag */ public static class ERXAttachmentExceedsLengthException extends ERXValidationException { /** * Do I need to update serialVersionUID? * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a> */ private static final long serialVersionUID = 1L; private long _maxSize; private String _recommendedFileName; public ERXAttachmentExceedsLengthException(String type, Object object, String key, long maxSize, String recommendedFileName) { super(type, object, key); _maxSize = maxSize; _recommendedFileName = recommendedFileName; } public void setMaxSize(long maxSize) { _maxSize = maxSize; } public long getMaxSize() { return _maxSize; } public void setRecommendedFileName(String recommendedFileName) { _recommendedFileName = recommendedFileName; } public String getRecommendedFileName() { return _recommendedFileName; } } }