Skip navigation links
Project Wonder 7.5-SNAPSHOT

Package er.attachment

See: Description

Package er.attachment Description

ERAttachment

Overview

It is a common requirement to be able to have arbitrary attachments of images, files, or documents throughout your WebObjects application. ERAttachment framework was inspired by the Rails attachment_fu gem to provide a simple mechanism of adding attachments to your application that abstract the actual storage requirements for those attachments. The framework provide a single unified set of components and models that can allow the storage of attachments on your local filesystem, served through your webserver; on your local filesystem served through a custom request handler; in your database, served through a custom request handler; on RackSpace's CloudFiles service; and on Amazon's S3 service, served directly from S3. The intent of the framework is to make it very simple to control how the attachments are stored and served by simple adjusting some configuration properties.

Installation and Setup

If you use Project Wonder migrations and migrateOnStartup, then the ERAttachment tables will be automatically created for you on the first launch. If you do not, then you can either manually execute the .migration SQL scripts that are in the Resources folder (execute them in numeric order), or you can open the EOModel and generate SQL for your particular database.

You will have to configure a connection dictionary for the ERAttachment model. You can do this either by setting global connection properties for all of your EOModels, or by setting the connection dictionary for just the ERAttachment model.

Global Database Properties

dbConnectURLGLOBAL
(optional) The jdbc URL for your database server.
dbConnectUserGLOBAL
(optional) The user for your database.
dbConnectPasswordGLOBAL
(optional) The password corresponding to your database user.

ERAttachment Model Properties

ERAttachment.URL
(optional) The jdbc URL for your database server.
ERAttachment.DBUser
(optional) The user for your database.
ERAttachment.DBPassword
(optional) The password corresponding to your database user.

Configuration

Each processor has different settings that are required to be able to perform an attachment import. A concept that is carried through all of them, however, is that of a "configuration". It's very likely that a simple application may have multiple attachment columns -- maybe a Person has an avatar, and a Bug has a screenshot. In this case, it is also often desirable to have different configuration sets for each type of attachment. All of the configuration settings can be overridden on a per configuration basis. For instance, if you set the value "er.attachment.s3.bucket" as the default bucket to put attachments into, you could also define a configuration "Person.avatar" (the actual String is unimportant, but a good idea is to make it EntityName.relationshipName to keep things clear) and then also define a property "er.attachment.s3.Person.avatar.bucket" which would override the bucket definition of any "Person.avatar" configuration. All of the ERAttachment components allow you to pass in a configurationName binding which corresponds to this concept.

Global Properties

There are some properties that apply to all attachment types.

er.attachment.maxSize / er.attachment.[configurationName].maxSize
(optional) The maximum size in bytes for an attachment upload. Currently this is only enforced after the upload has finished. If you are using AjaxFileUpload, you can also set the max size for Ajax uploads which is checked during the upload, although currently there are no per-configation overrides for this. This behavior will soon be cleaned up and unified so that the max size is enforced prior to and during the upload with configuration overrides. The default is unlimited.
er.attachment.tempFolder / er.attachment.[configurationName].tempFolder
(optional) For WOFileUpload (not Ajax), this specifies the temp folder to write incoming uploads into. For Ajax, use the settings defined by AjaxFileUpload component and AjaxFileUploadRequestHandler. The default is the Java definition of the temp folder (/tmp for Mac OS X and UNIXes).
er.attachment.storageType / er.attachment.[configurationName].storageType
(optional) When using the ERAttachmentUpload component, this sets the default storage type to use for the attachment. Examples are "cf", "s3", "db", or "file". The default is "db".
er.attachment.width / er.attachment.[configurationName].width and er.attachment.height / er.attachment.[configurationName].height
(optional) When using the ERAttachmentUpload component, this sets the width and/or height of the attachment. Currently this will resize the original attachment (i.e. the original is lost). Later versions will support generating thumbnails in addition to the original image.
er.attachment.proxyAsAttachment / er.attachment.[configurationName].proxyAsAttachment
(optional) When using the proxy request handler, this sets whether or not the request handler returns an attachment and Content-Disposition header. This will typically cause the browser to download the attachment versus displaying inline. Defaults false.

Image Processing Properties

While ERAttachment will try and automatically figure out a library for image processing operations such as thumbnailing and resizing, you can configure it yourself using appropriate properties. Here are some properties that apply to image processing libraries configuration.

er.attachment.thumbnail.imageProcessor
Values for this property can be one of sips, imageio, imagemagick or java
er.attachment.ImageProcessor.imageMagickBinFolder
Indicates the directory path of the ImageMagick binaries.

Path Templates

Several attachment types define filesystem and webserver paths for storing and accessing attachments. A very simple set of template variables are defined for use in the definition of these paths.

${ext}
Attempts to lookup the mime type of the original file (based on its original extension) and returns the primary file extension for that mime type including a "." prefix. For instance, if a user uploads an image/jpg attachment with the original extension .JPEG, the variable replacement would be ".jpg". If there is no mime type found, or the mime type does not have a primary extension, the original extension of the file is used. If there is no original extension, this variable will replace with a blank (i.e. no dot).
${hash}
Produces a series of three folders based on the SHA-1 hash of the original file name ("a/c/9"). This is particularly useful if you are going to have a lot of attachments, because this keeps down the number of files in any one directory.
${uuid}
Produces a unique UUID using java.util.UUID. This is useful if you just want your path to contain a unique value that is not related to the attachment itself.
${pk}
Replaced by the primary key of the ERAttachment object.
${fileName}
Replaced by the original filename as uploaded by the user (including the file extension).

Database Attachments

The easiest attachment type to use, because it has the least amount of setup required is a database attachment. Database attachments are stored in the database in the ERAttachmentData table. Currently this is not using a method like the C9ResourceStreamer, which could stream in and out of the a database column, rather it is using normal EOF NSData blob columns. Because of the memory characteristics of blob columns in EOF, this attachment type is only suitable for relatively small data sizes. All database attachments are served through the ERAttachmentRequestHandler. Because it is proxied, database attachments provide the capability of security checking access to the attachments from within your WebObjects applications (see the section on security below for more info).

er.attachment.db.webPath / er.attachment.[configurationName].db.webPath
(optional) The path that will be appended to the URL after the "/attachments" request handler key. Defaults to "/${pk}${ext}". webPaths must be unique across all attachments (including ${pk} ensures this).
er.attachment.db.smallData / er.attachment.[configurationName].db.smallData
(optional) Because of the memory characteristics of EOF and large BLOBs, a common design for storing data in an EO is to have a to-one relationship that contains the data. This requires an extra query to retrieve the EO that contains the data, but provides better memory performance. However, if you are storing very small amounts of data -- for instance, an avatar icon for a user which may only be 2k, you may decide it's not worth it to require the extra query to get the attachment data. In this case, you can set smallData = true, and the data will be stored in the actual attachment EO rather than in a second separated EO. The default is false.

File Attachments

File attachments give a much greater level of flexibility compared to database attachments. In particular, file attachments can be optionally not proxied. A proxied attachment is one that is served through the ERAttachmentRequestHandler, and is streamed to the user by your WebObjects application. A non-proxied attachment is served directly as a static file through Apache (or whatever webserver you are using). To be able to use file attachments, your WebObjects application must have write permission to whatever folders and subfolders you specify as filesystemPath. Additionally, if you are using non-proxied attachments, those folders must be externally visible from your webserver (that is, the filesystemPath must be a subfolder of a document root). Proxied attachments also provide the capability of security checking access to the attachments from within your WebObjecst applications (see the section on security below for more info).

er.attachment.file.proxy / er.attachment.[configurationName].file.proxy
(optional) If proxy is true, then the attachment's contents will be served through the request handler proxy. The default is true.
er.attachment.file.overwrite / er.attachment.[configurationName].file.overwrite
(optional) If you specify a filesystemPath based on ${fileName}, it is possible that you may have duplicates. If overwrite is true, then duplicates will be overwritten. If false, then a unique name based on the template will be created using a "filename-1.gif", "filename-2.gif" scheme. The default is false.
er.attachment.file.filesystemPath / er.attachment.[configurationName].file.filesystemPath
(required) The filesystem path specifies the full path of the destination of the uploaded attachment, including its filename. This property is evaluated as a path template (see above). An example might be "/Library/WebServer/Documents/photos/${hash}/${pk}${ext}".
er.attachment.file.webPath / er.attachment.[configurationName].file.webPath
(required if proxy is false) The web path specifies the path relative to the webserver document root (or request handler if proxy is true) used to locate the given attachment. webPaths must be unique across all attachments. If proxy is false, the default webPath is "/${pk}{$ext}". In the above example of a filesystemPath, if proxy is false, then the appropriate webPath would be "/photos/${hash}/${pk}${ext}".

S3 Attachments

Amazon's S3 service is an incredibly cost-effective way to host large amounts of data, which is ideal if you are offering an attachment system for a public website. For information about signing up for an account, go to Amazon's S3 Page. Because S3 uploading may take a while, the actual upload to S3 is performed in a background queue. As a result, your attachment may be in an "unavailable" state (attachment.available = false) until the upload is complete. The attachment components handle this state with specific views and by disabling attachment links. Note: Deleting an attachment that is in the unavailable state may result in the file not being removed from S3 right now.

er.attachment.s3.accessKeyID / er.attachment.[configurationName].s3.accessKeyID
(required) Your Amazon S3 access key ID. This is loaded using ERXProperties.decryptedStringForKey if you would like to encrypt it.
er.attachment.s3.secretAccessKey / er.attachment.[configurationName].s3.secretAccessKey
(required) Your Amazon S3 secret access key. This is loaded using ERXProperties.decryptedStringForKey if you would like to encrypt it.
er.attachment.s3.bucket / er.attachment.[configurationName].s3.bucket
(required) The name of the bucket to store and retrieve attachments into. The bucket must already exist in your S3 account.
er.attachment.s3.host / er.attachment.[configurationName].s3.host
(optional) Defaults to s3.amazonaws.com. Use this property if your bucket is not in the default region. For example put s3-eu-west-1.amazonaws.com for EU(Ireland).
er.attachment.s3.key / er.attachment.[configurationName].s3.key
(optional) The name of the file to store in the S3 bucket. This is evaluated as a path template. The default value is "${pk}${ext}".
er.attachment.s3.acl / er.attachment.[configurationName].s3.acl
(optional) The access control policy for uploaded objects. See http://docs.amazonwebservices.com/AmazonS3/latest/index.html?RESTAccessPolicy.html for more deails. The default value is "public-read".
er.attachment.s3.linkLife / er.attachment.[configurationName].s3.linkLife
(optional) If acl is set to "private" then urls generated for attachments are signed and expire. linkLife specifies the length of time in milliseconds that the generated url is valid. The default value is "60000".
er.attachment.s3.proxy / er.attachment.[configurationName].s3.proxy
(optional) If proxy is true, then the attachment's contents will be served through the request handler proxy. The default is true.

CloudFiles Attachments

RasckSpace's CloudFiles service is an alternative to Amazon's S3. Like the S3 implementation, uploading may take a while, so the actual upload to CloudFiles is performed in a background queue. As a result, your attachment may be in an "unavailable" state (attachment.available = false) until the upload is complete. The attachment components handle this state with specific views and by disabling attachment links. Note: Deleting an attachment that is in the unavailable state may result in the file not being removed from CloudFiles right now.

er.attachment.cf.apiAccessKey / er.attachment.[configurationName].cf.apiAccessKey
(required) Your CloudFiles API access key ID. This is loaded using ERXProperties.decryptedStringForKey if you would like to encrypt it.
er.attachment.cf.username / er.attachment.[configurationName].cf.username
(required) Your CloudFiles username. This is loaded using ERXProperties.decryptedStringForKey if you would like to encrypt it.
er.attachment.cf.container / er.attachment.[configurationName].cf.container
(required) The name of the container to store and retrieve attachments into. The container must already exist in your CloudFiles account.
er.attachment.cf.connectionTimeOut / er.attachment.[configurationName].cf.connectionTimeOut
(optional) The timeout, in ms, before it stops trying to reach CloudFiles servers. Default is 5000
er.attachment.cf.authUrl / er.attachment.[configurationName].cf.authUrl
(optional) URL to the REST API. Default is https://auth.api.rackspacecloud.com/v1.0

Mime Types

The ERAttachment framework provides support for tracking the definition of various mime types. You can, however, override and extend the default definitions. The mime type of an attachment determines which viewer will be used to display the attachment.

er.attachment.mimeTypes
(optional) A comma-separated list of mime types that are defined in the system. The default is "image/bmp,image/eps,image/gif,image/jp2,image/jpeg,image/pdf,image/pict,image/png,image/psd,image/raw,image/tiff,image/x-pict,text/plain,text/html,text/xml,text/x-diff,application/zip,application/x-tar,application/x-gzip,application/x-octet-stream,video/quicktime". If you set this value in your application, it will override the list defined in the framework.
er.attachment.additionalMimeTypes
(optional) Just like mimeTypes except it is intended to extend the core list in er.attachment.mimeTypes rather than replace them.

For each mime type defined in the mimeTypes and additionalMimeTypes properties, there are several additional properties you must specify.

er.attachment.mimeType.[mimeType].name
(optional) The display name of the mime type ("Portable Network Graphics")
er.attachment.mimeType.[mimeType].uti
(optional) The universal type identifier that corresponds to this mime type
er.attachment.mimeType.[mimeType].extensions
(required) A comma-separated list of file extensions that correspond to this mime type. The first entry in the list will be treated as the "primary extension" and is used by, for instance, the ${ext} path template variable.

Attachment Variations

Currently there is no support for automatically thumbnailing of attachments or other attachment variations, however the model was written with the explicit intent of adding this support.

Security

If you are serving S3 or non-proxied file attachments, security is controlled by S3 and your web server respectively. However, if you are serving proxied attachments like database attachments or proxied file attachments, you can provide a delegate to implement security checks on your attachment. Currently the delegate is very simple and does not provide much context for the request, but the delegate features will be extended over time.

To provide a delegate, you must implement the ERAttachmentRequestHandler.Delegate interface, and in your application constructor, you can call:

((ERAttachmentRequestHandler) WOApplication.application().requestHandlerForKey(ERAttachmentRequestHandler.REQUEST_HANDLER_KEY)).setDelegate(yourCustomDelegate);

The delegate interface has one method, which is passed the ERAttachment that the user requested along with the WORequest and WOContext, which you can use to lookup session information as well as authentication and authorization information. You can use the configurationName() property of ERAttachment to determine which security context you are evaluating the attachment in.

Modeling Attachments

To use attachments, you will need to add support for one or more attachment relationships in your EOModel. Let's take the example of a Person with an avatar image. In our Person EO, we would add:

  1. An non-class, allows null property "avatarID" (of type "id" if you use ERPrototypes)
  2. An optional relationship "avatar" that joins Person.avatarID to ERAttachment.id

And that's it ... You're done modeling.

Uploading Attachments

Uploading attachments into the system is very easy. You can either use the ERAttachmentUpload component, which provides a very thin layer of features on top of either a WOFileUpload or an AjaxFileUpload component; or you can use your own WOFileUpload and call the attachment import API directly.

Let's look at the case of uploading an avatar attachment for our Person again. We want to upload our avatars to S3 using a non-Ajax file upload in this example.

Properties

er.attachment.Person.avatar.storageType=s3 er.attachment.Person.avatar.s3.accessKeyID=xxxxxxxxxx er.attachment.Person.avatar.s3.secretAccessKey=xxxxxxxxxxxxxxx er.attachment.Person.avatar.s3.bucket=Avatars

WOD

AvatarForm : WOForm { enctype = "multipart/form-data"; multipleSubmit = true; } AvatarUpload : ERAttachmentUpload { configurationName = "Person.avatar"; editingContext = person.editingContext; attachment = person.avatar; } UploadAction : WOSubmitButton { action = uploadAvatar; value = "Upload Avatar!"; }

And that's it. When the file is uploaded, it will be processed and posted to S3. You can override "storageType" on the ERAttachmentUpload, as well as "mimeType" if you are only uploading specific mimeTypes. To better implement security in your request handler delegate, it is also possible to specify an "ownerID" binding on the upload. This ownerID should be a string that corresponds to the ID of the "owner" of this attachment (like "person.primaryKey" in our avatar example). This provides a "loose" foreign key back to the object this attachment is associated with, providing a mechanism to look up the owner during the security checking process to get a better context of the request.

However, you may want to use your own WOFileUpload in a form, or maybe you have an existing upload process. In this case, you will want to call the API directly. Fortunately this is very simple to do:

ERAttachment attachment = ERAttachmentProcessor.processorForType(storageType).process(editingContext, uploadedFile, originalFileName, mimeType, configurationName, ownerID);

In this example, storageType would be "s3", editingContext would be the editing context to create the attachment within, uploadedFile would be a java.io.File reference to the temporary file (which will be deleted after processing), originalFileName is the name the user gave the attachment on upload, mimeType can be null or set explicitly (if null, it will be guessed from the originalFileName extension), configurationName is the name of the configuration properties to use, and ownerID is the optional ID of the object that "owns" this attachment (see the description of the ownerID binding on ERAttachmentUpload for more details).

Note: Once you select the storageType for an avatar, there are currently no tools for changing your mind later. You would need to manually edit the ERAttachment objects if you want to, for instance, move your S3 attachments back onto a local machine and convert them to file attachments. The API provides most of the underlying tools to do the data management, but the grunt work of actually switching everything around is left as an exercise for the reader :)

Viewing Attachments

There are two ways to view an attachment -- linking and embedding. To embed an attachment in the page (assuming it is an embeddable type), you can use the ERAttachmentViewer component. If the attachment type is not one that has a renderer, ERAttachmentViewer will display a default icon instead. If you only want to provide a link, ERAttachmentLink works basically link a WOHyperlink except that you provide a configurationName and attachment reference. Additionally, you can use ERAttachmentIcon to show an icon based on the mime type of the attachment.

Avatar : ERAttachmentViewer { configurationName = "Person.avatar"; attachment = person.avatar; width = 50; } AvatarLink : ERAttachmentLink { configurationName = "Person.avatar"; attachment = person.avatar; }

Custom Viewers

Viewers for attachments are pluggable. You can create a component that extends AbstractERAttachmentViewer, and set some a configuration property that maps a mime type to your viewer.

er.attachment.mimeType.[mimeType].viewer
(optional) The class name of the viewer component to display for a particular mime type. You can set explicit mime types or "glob" mime types. For instance, you can set er.attachment.mimeType.image/pdf.viewer=com.mine.PDFViewer as well as er.attachment.mimeType.image/*.viewer=com.mine.DefaultImageViewer.
er.attachment.mimeType.default.viewer
(optional) The class name of the viewer component to display when no other mimetype-specific viewer matches.
er.attachment.mimeType.unavailable.viewer
(optional) The class name of the viewer component to display when an attachment is in the "unavailable" state.

As an example, the default Properties for the framework define:

er.attachment.mimeType.image/pdf.viewer=er.attachment.components.viewers.ERAttachmentDefaultViewer er.attachment.mimeType.image/*.viewer=er.attachment.components.viewers.ERAttachmentImageViewer er.attachment.mimeType.default.viewer=er.attachment.components.viewers.ERAttachmentDefaultViewer er.attachment.mimeType.unavailable.viewer=er.attachment.components.viewers.ERAttachmentUnavailableViewer

Custom Storage

ERAttachment is written with a modular design. If you want to provide your own storage locations, you can:

  1. Add a single-table inheritance ERAttachment subclass with a custom storageType.
  2. Add a custom ERAttachmentProcessor subclass that can import into, and provide URLs onto, the storage location.
Skip navigation links
Last updated: Sun, Dec 22, 2024 • 04:08 PM CET

Copyright © 2002 – 2024 Project Wonder.