/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. */ package tufts.vue; import tufts.Util; import java.io.File; import java.io.StringWriter; import java.io.PrintWriter; import java.net.URI; import java.net.URL; import java.awt.Image; import java.util.Properties; import tufts.vue.ui.ResourceIcon; import javax.swing.JComponent; import javax.swing.Icon; import javax.swing.ImageIcon; /** * The Resource abstract class defines a set of methods which all VUE Resource objects * must implement, and provides basic common functionality to all Resource types. * This class create a uniform way to handle dragging and dropping of resource * objects, displaying their content, and fetching their data. * * @version $Revision: 1.97 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $ */ public abstract class Resource implements Cloneable { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(Resource.class); /** property keys with this prefix will be persisted, but are not meant for display to users */ public static final String HIDDEN_PREFIX = "@"; /** property keys with this prefix are for runtime display only: will not be persisted */ public static final String RUNTIME_PREFIX = "~"; /** property keys with this prefix are for internal runtime use only: will not be displayed or persisted */ public static final String HIDDEN_RUNTIME_PREFIX = "@@"; /** property keys with this prefix are both hidden and runtime-only */ public static final String DEBUG_PREFIX = "##"; public static final String PACKAGE_FILE = HIDDEN_RUNTIME_PREFIX + "package.file"; public static final String PACKAGE_KEY_DEPRECATED = HIDDEN_PREFIX + "Packaged"; //public static final String PACKAGE_FILE = DEBUG_PREFIX + "package.file"; public static final String PACKAGE_ARCHIVE = RUNTIME_PREFIX + "Package"; // Some standard property names public static final String CONTENT_SIZE = "Content.size"; public static final String CONTENT_TYPE = "Content.type"; public static final String CONTENT_MODIFIED = "Content.modified"; public static final String CONTENT_ASOF = "Content.asOf"; public static final String CONTENT_SOURCE = "Content.source"; public static final String IMAGE_FORMAT = "image.format"; public static final String IMAGE_WIDTH = "image.width"; public static final String IMAGE_HEIGHT = "image.height"; public static final Object MANAGED_UNMARSHALLING = "MANAGED-UNMARSHALLING"; public static final java.awt.datatransfer.DataFlavor DataFlavor = tufts.vue.gui.GUI.makeDataFlavor(Resource.class); public static final String SPEC_UNSET = "<spec-unset>"; public static boolean isHiddenPropertyKey(String key) { char c = 0; try { c = key.charAt(0); } catch (Throwable t) {} return c == '@' || c == '#'; } /** runtime property keys do *not* persist */ public static boolean isRuntimePropertyKey(String key) { try { final char c0 = key.charAt(0); final char c1 = key.charAt(1); return c0 == '~' || c0 == '#' || (c0 == '@' && c1 == '@'); } catch (Throwable t) { //if (DEBUG.Enabled) Log.warn("short key? " + Util.tags(key) + "; " + t); } return false; } /** @return true if this is not a user property (e.g., is hidden or runtime) */ public static boolean isInternalPropertyKey(String key) { try { final char c = key.charAt(0); return c == '@' || c == '#' || c == '~'; } catch (Throwable t) { //if (DEBUG.Enabled) Log.warn("short-key? " + Util.tags(key) + "; " + t); } return false; } /** * Interface for Resource factories. All methods are "get" based as opposed to "create" * as the implementation may optionally provide resources on an atomic basis (e.g., all equivalent URI's / URL's * may return the very same object. * * TODO: handle Fedora AssetResource? Are we still using that, or has it been implemented as a generic OSID? * If not being used, see if we can remove it from the codebase... * */ public static interface Factory { Resource get(String spec); Resource get(java.net.URL url); Resource get(java.net.URI uri); Resource get(java.io.File file); Resource get(osid.filing.CabinetEntry entry); Resource get(org.osid.repository.Repository repository, org.osid.repository.Asset asset, org.osid.OsidContext context) throws org.osid.repository.RepositoryException; Resource get(org.xml.sax.InputSource s); } /** A default resource factory: does basic delgation by type, but only handle's absolute resources (e.g., does no relativization) */ public static class DefaultFactory implements Factory { public Resource get(String spec) { // if spec looks a URL/URI or File, could attempt to construct such and if succeed, // pass off to appropriate factory variant. Wouldn't be worth anything at moment // as they all pretty much do the same thing for now... return postProcess(URLResource.create(spec), spec); } public Resource get(java.net.URL url) { return postProcess(URLResource.create(url), url); } public Resource get(java.net.URI uri) { return postProcess(URLResource.create(uri), uri); } public Resource get(java.io.File file) { // someday this may return something like a FileResource (which could make // use of a LocalCabinetResource, if we upgraded that API to be useful // and handle things like fetch file typed icon images, etc). return postProcess(URLResource.create(file), file); } public Resource get(osid.filing.CabinetEntry entry) { return postProcess(CabinetResource.create(entry), entry); } public Resource get(org.osid.repository.Repository repository, org.osid.repository.Asset asset, org.osid.OsidContext context) throws org.osid.repository.RepositoryException { if (DEBUG.RESOURCE) System.out.println(""); // for spacing out large sets of search results Resource r = new Osid2AssetResource(asset, context); try { //if (DEBUG.DR && repository != null) r.addProperty("~Repository", repository.getDisplayName()); if (repository != null) { r.setHiddenProperty("osid.impl", repository.getClass().getName()); r.setHiddenProperty("osid.name", repository.getDisplayName()); //r.setHiddenProperty("osid.id", repository.getProviderId()); // not on interface? } } catch (Throwable t) { Log.warn(Util.tags(r), t); } return postProcess(r, asset); } public Resource get(org.xml.sax.InputSource s) { // if spec looks a URL/URI or File, could attempt to construct such and if succeed, // pass off to appropriate factory variant. Wouldn't be worth anything at moment // as they all pretty much do the same thing for now... String spec = s.getSystemId(); if (spec == null) spec = s.getPublicId(); if (spec == null) { Log.error("no system or public id in " + Util.tags(s) + "; can't create resource"); return null; } return postProcess(URLResource.create(s.getSystemId()), s); } protected Resource postProcess(Resource r, Object source) { r.setReferenceCreated(System.currentTimeMillis()); if (DEBUG.DR) Log.debug(Util.tags(source) + " -> " + Util.tags(r)); //Log.debug("Created " + Util.tags(r) + " from " + Util.tags(source)); return r; } } private static final Factory AbsoluteResourceFactory = new DefaultFactory(); /** @return the default resource factory */ // could allow installation of a new default factory (be sure to make installation threadsafe if do so) public static Factory getFactory() { return AbsoluteResourceFactory; } // Convenience factory methods: guaranteed equivalent to getFactory().get(args...) // If an LWMap ResourceFactory is available, that should always be used instead of these. public static Resource instance(String spec) { return getFactory().get(spec); } public static Resource instance(java.net.URL url) { return getFactory().get(url); } public static Resource instance(java.net.URI uri) { return getFactory().get(uri); } public static Resource instance(java.io.File file) { return getFactory().get(file); } public static Resource instance(osid.filing.CabinetEntry entry) { return getFactory().get(entry); } public static Resource instance(org.osid.repository.Repository repository, org.osid.repository.Asset asset, org.osid.OsidContext context) throws org.osid.repository.RepositoryException { return getFactory().get(repository, asset, context); } public static Resource instance(org.xml.sax.InputSource s) { return getFactory().get(s); } /* * The set of types might ideally be defined / used by clients and subclass impl's, * not enumerated in the Resource class, but we're keeping this around * for old code and given that this info is actually persisted in save files * going back years. Tho given the way we're currently using these types, * we can probably get rid of them / ignore old persisted values if we * get time to clean this up. SMF 2007-10-07 */ /* Some client type codes defined for resources -- TODO: fix this -- there are mixed semantics here */ static final int NONE = 0; // Unknown type. static final int FILE = 1; // Resource is a Java File object. static final int URL = 2; // Resource is a URL. public static final int DIRECTORY = 3; // Resource is a directory or folder. static final int FAVORITES = 4; // Resource is a Favorites Folder static final int ASSET_OKIDR = 10; // Resource is an OKI DR Asset. static final int ASSET_FEDORA = 11; // Resource is a Fedora Asset. static final int ASSET_OKIREPOSITORY = 12; // Resource is an OKI Repository OSID Asset. protected static final String[] TYPE_NAMES = { "NONE", "FILE", "URL", "DIRECTORY", "FAVORITES", "unused5", "unused6", "unused7", "unused8", "unused9", "ASSET_OKIDR", "ASSET_FEDORA", "ASSET_OKIREPOSITORY" }; /** the metadata property map -- should be final, but not because of clone support **/ /*final*/ protected MetaMap mProperties = new MetaMap(); static final long SIZE_UNKNOWN = -1; private int mType = Resource.NONE; private long mByteSize = SIZE_UNKNOWN; private long mReferenceCreated; private long mAccessAttempted; private long mAccessSuccessful; //---------------------------------------------------------------------------------------- // Standard methods for all Resources //---------------------------------------------------------------------------------------- public long getByteSize() { return mByteSize; } /** @return null if SIZE_UNKOWN, otherwise a valid Long */ public Long getPersistByteSize() { return mByteSize == SIZE_UNKNOWN ? null : Long.valueOf(mByteSize); } protected void setByteSize(long size) { if (DEBUG.RESOURCE) dumpField("setByteSize", size); mByteSize = size; } // todo: generic inteferace setClientData / getClientData (the LWComponent can use also), // that uses a key/value map with only a single value per key (NOT a multi-map) /** * Set the given property value. * Does nothing if either key or value is null, or value is an empty String. */ public void setProperty(String key, Object value) { if (key == null) return; if (DEBUG.DATA) dumpKV("setProperty", key, value); mProperties.setNonEmpty(key, value); // if (value != null) { // if (DEBUG.DATA) dumpKV("setProperty", key, value); // if (!(value instanceof String && ((String)value).length() < 1)) // mProperties.put(key, value); // } else { // if (DEBUG.Enabled) { // if (mProperties.containsKey(key)) { // Object o = mProperties.get(key); // dumpKV("setProperty(null)overwite?", key, o); // } // } // } } public void setProperty(String key, long value) { // if (key.endsWith(".contentLength") || key.endsWith(".size")) { // // this is a hack to handle HTTP header info // setByteSize(value); // } setProperty(key, Long.toString(value)); } protected void dumpField(String name, Object value) { out(String.format("%-31s: %s%s%s", name, Util.TERM_CYAN, Util.tags(value), Util.TERM_CLEAR)); } private void dumpKV(String name, String key, Object value) { out(String.format("%-14s%17s: %s%s%s", name, key, Util.TERM_RED, Util.tags(value), Util.TERM_CLEAR)); } /** runtime properties are for display while VUE is running only: they're not persisted */ protected void setRuntimeProperty(String key, Object value) { setProperty(RUNTIME_PREFIX + key, value); } /** hidden properties are not displayed at runtime, although they are persisted */ public void setHiddenProperty(String key, Object value) { setProperty(HIDDEN_PREFIX + key, value); } /** debug properties are neither displayed at runtime, nor persisted */ protected void setDebugProperty(String key, Object value) { setProperty(DEBUG_PREFIX + key, value); } /** @return any prior value stored for this key, null otherwise */ public Object removeProperty(String key) { Object o = mProperties.remove(key); if (DEBUG.DATA && o != null) dumpKV("removeProperty", key, o); return o; } public void addProperty(String key, Object value) { if (DEBUG.DATA) dumpKV("addProperty", key, value); mProperties.add(key, value); } // /** // * Add a property with the given key. If a key already exists // * with this name, the key will be modified with an index. // */ // public String addProperty(String desiredKey, Object value) { // if (DEBUG.DATA) dumpKV("addProperty", desiredKey, value); // return mProperties.addProperty(desiredKey, value); // } public void addPropertyIfContent(String key, Object value) { if (DEBUG.DATA) dumpKV("addPropertyIf", key, value); mProperties.putNonEmpty(key, value); } // public String addPropertyIfContent(String desiredKey, Object value) { // if (DEBUG.DATA) dumpKV("addPropertyIf", desiredKey, value); // return mProperties.addIfContent(desiredKey, value); // } public Object getPropertyValue(String key) { final Object value = mProperties.getValue(key); if (DEBUG.DATA && value != null) dumpKV("getProperty", key, value); return value; } /** * This method returns a value for the given property name. * @param pName the property name. * @return Object the value **/ public String getProperty(String key) { final Object value = getPropertyValue(key); return value == null ? null : value.toString(); } // /** @return the value found for the first key finding a non-null value */ // public String getProperty(String... keys) { // return mProperties.getValue(keys); // } public int getProperty(String key, int notFoundValue) { final Object value = mProperties.get(key); int intValue = notFoundValue; if (value != null) { if (value instanceof Number) { intValue = ((Number)value).intValue(); } else if (value instanceof String) { try { intValue = Integer.parseInt((String)value); } catch (NumberFormatException e) { if (DEBUG.DATA) tufts.Util.printStackTrace(e); } } } return intValue; } public boolean hasProperty(String key) { return mProperties.hasKey(key); } public MetaMap getProperties() { return mProperties; } public long getReferenceCreated() { return mReferenceCreated; } public void setReferenceCreated(long created) { mReferenceCreated = created; } public long getAccessAttempted() { return mAccessAttempted; } public void setAccessAttempted(long attempted) { mAccessAttempted = attempted; } public Long getPersistAccessAttempted() { return mAccessAttempted == 0 ? null : Long.valueOf(mAccessAttempted); } public void setPersistAccessAttempted(Long l) { setAccessAttempted(l == null ? 0 : l.longValue()); } public long getAccessSuccessful() { return mAccessSuccessful; } public void setAccessSuccessful(long succeeded) { mAccessSuccessful = succeeded; } public Long getPersistAccessSuccessful() { return mAccessSuccessful == 0 ? null : Long.valueOf(mAccessSuccessful); } public void setPersistAccessSuccessful(Long l) { setAccessSuccessful(l == null ? 0 : l.longValue()); } protected void markAccessAttempt() { setAccessAttempted(System.currentTimeMillis()); } protected void markAccessSuccess() { setAccessSuccessful(System.currentTimeMillis()); } /** for castor hacks */ public Object getNull() { return null; } //public abstract boolean isImage(); private boolean isImage; /** @return true if this resource contains displayable image data */ public boolean isImage() { return isImage; } protected void setAsImage(boolean asImage) { isImage = asImage; if (DEBUG.RESOURCE) setDebugProperty("isImage", ""+ asImage); } /** init pass to run after de-serialization (optional until we know we want to keep the resource) */ protected void initAfterDeserialize(Object context) {} /** final init pass to run after de-serialization (optional until we know we want to keep the resource) */ protected void initFinal(Object context) {} /** * @return true if the data behind this component has recently changed * * This impl always returns false. Override to provide desired semantics. */ public boolean dataHasChanged() { return false; } /** * @return an object suitable to be handed to the Java ImageIO API that can * in some way be read and converted to an image: e.g., java.net.URL, java.io.File, * java.io.InputStream, javax.imageio.stream.ImageInputStream, etc. * If the object provides a convenient, unique, persisent key, such as URL or File, * the VUE Images code can use that to cache the result on disk. * May return null if no image is available. */ public abstract Object getImageSource(); /** @return a current local File, if there is one, that contains the data for this object. * E.g., could return an original local data file, an http: cache file, etc. * Return null if no such file exists. */ protected File mDataFile; public File getActiveDataFile() { File sourceFile; if (mDataFile != null) { sourceFile = mDataFile; } else if (isImage()) { sourceFile = Images.findCacheFile(this); } else { sourceFile = null; } return sourceFile; } public void flushCache() { try { Images.flushCache(getActiveDataFile()); } catch (Throwable t) { Log.warn(this, t); } } private boolean isCached; protected boolean isCached() { return isCached; } // todo: this should be computed internally (move code out of Images.java) public void setCached(boolean cached) { isCached = cached; } /** * Return the title or display name associated with the resource. * (any length restrictions?) */ public abstract String getTitle(); public abstract void setTitle(String title); /** @return some kind of reliable name: e.g., title if there is one, spec if not */ public String getName() { final String title = getTitle(); if (title == null || title.trim().length() < 1) return getSpec(); else return title; } //public abstract long getSize(); /** * Return a resource reference specification. This could be a filename or URL. */ // todo: replace with more abstract call(s) -- as of packaging, callers // don't generally just want the "spec", they want a local file if we // have one (including a possible redirect to a package file) or a URL // reference, with different priorities in different cases. public abstract String getSpec(); // /** // * If a reference to this resource can be provided as a URL, return it in that form, // * otherwise return null. // * // * @return default Resource class impl: returns null // */ // public java.net.URL asURL() { // return null; // } // /** // * All Resource impls should be able to return something that fits into a URI. // */ // public abstract java.net.URI toURI(); /** * Return the resource type. This should be one of the types defined above. */ public int getClientType() { return mType; } /** TODO: need to remove this -- if we keep any type at all, it should at least be inferred * -- probably replace with a setClientType(Object) -- a general marker that clients / UI components can use */ public void setClientType(int type) { mType = type; if (DEBUG.RESOURCE){ if (DEBUG.META) dumpField("setClientType", TYPE_NAMES[type] + " (" + type + ")"); try { setDebugProperty("clientType", TYPE_NAMES[type] + " (" + type + ")"); } catch (Throwable t) { Log.warn(this + "; setClientType " + type, t); } } } /** * Display the content associated with the resource. For example, call * VueUtil.open() using the spec information. */ public abstract void displayContent(); protected String mExtension = null; public void reset() { mExtension = null; } public void setDataType(String s) { mExtension = s; } private static final String NO_EXTENSION = "<no-ext>"; public static final String EXTENSION_DIR = "dir"; public static final String EXTENSION_HTTP = "web"; public static final String EXTENSION_UNKNOWN = "---"; public static final String EXTENSION_VUE = "vue"; /** * Return a filename extension / file type of this resource (if any) suitable for identify it * it's "type" to the local file system shell environement (e.g., txt, html, jpg). */ public final String getDataType() { if (mExtension == null) { // This code is safe to run more than once / in multiple // threads as the cached result should always be the same, // so we don't need to synchronized this lazy-eval code. mExtension = determineDataType(); if (DEBUG.RESOURCE) setDebugProperty("dataType", mExtension); } return mExtension; } protected String determineDataType() { String ext; if (getClientType() == DIRECTORY) ext = EXTENSION_DIR; else ext = extractExtension(); if (ext == null || ext == NO_EXTENSION || ext.length() == 0) { final String spec = getSpec(); if (spec == null || spec == SPEC_UNSET || spec.trim().length() < 1) ext = EXTENSION_UNKNOWN; else if (spec.startsWith("http:") || spec.startsWith("https:")) ext = EXTENSION_HTTP; else if (getClientType() == Resource.FILE) ext = "txt"; else ext = EXTENSION_DIR; } else { ext = ext.toLowerCase(); if ("readme".equals(ext) || "msg".equals(ext)) { ext = "txt"; } else if (getClientType() == URL || getClientType() >= ASSET_OKIDR) { if ("asp".equals(ext) || // microsoft web page "php".equals(ext) || "pl".equals(ext) // commonly: a perl script ) { ext = EXTENSION_HTTP; } } } return ext; } protected String extractExtension() { return extractExtension(getSpec()); } /** @return the likely extension for the given string, or NO_EXTENSION if none found */ protected String extractExtension(final String s) { String ext = NO_EXTENSION; final char lastChar; if (s == null || s.length() == 0) lastChar = 0; else lastChar = s.charAt(s.length()-1); if (lastChar == 0) { // default NO_EXTENSION } else if ( ! Character.isLetterOrDigit(lastChar)) { // assume some kind of a path element (e.g., /, \): e.g.: no extension available } else { final int lastDotIdx = s.lastIndexOf('.'); // must have at least one char's worth of file-name, and one two chars worth of data after the dot if (lastDotIdx > 1 && (s.length() - lastDotIdx) > 2) { String extTest = s.substring(lastDotIdx + 1); // if first character of extension is not a letter or // digit, assome we have NOT found a real extension if (Character.isLetterOrDigit(extTest.charAt(0))) ext = extTest; } } if (DEBUG.RESOURCE) Log.debug("extractExtension " + this + "; [" + s + "] = [" + ext + "]"); return ext; } // // was getExtension // // TODO: cache / allow setting (e.g. special data sources might be able to indicate type that's otherwise unclear // // e.g., a URL query part that requests "type=jpeg") // public String getDataType() { // // String type = null; // // if (getClientType() == DIRECTORY) // // type = EXTENSION_DIR; // // else // // type = extractExtension(getSpec()); // String ext = extractExtension(getSpec()); // if (ext == null || ext == NO_EXTENSION || ext.length() == 0) { // final String spec = getSpec(); // if (spec == null || spec == SPEC_UNSET || spec.trim().length() < 1) // ext = EXTENSION_UNKNOWN; // else if (spec.startsWith("http:")) // todo: https, etc... // ext = EXTENSION_HTTP; // else // ext = EXTENSION_DIR; // } // // if (type == NO_EXTENSION) { // // if (getClientType() == FILE) { // // // todo: this really ought to be in a FileResource and/or a useful osid filing impl // // type = EXTENSION_DIR; // // // assume a directory for now... // // } // // } // if (DEBUG.RESOURCE) out("extType=[" + ext + "] in " + this); // //if (DEBUG.RESOURCE) out(getSpec() + "; extType=[" + ext + "] in [" + this + "] type=" + TYPE_NAMES[getClientType()]); // return ext; // } private ImageIcon mTinyIcon; /** @return a 16x16 icon */ public Icon getTinyIcon() { if (mTinyIcon == null) mTinyIcon = makeIcon(16, 16); return mTinyIcon; } private ImageIcon mLargeIcon; /** @return up to a 128x128 icon */ public Icon getLargeIcon() { if (mLargeIcon == null) mLargeIcon = makeIcon(32, 128); return mLargeIcon; } private ImageIcon makeIcon(int size, int max) { final String ext = getDataType(); Image image = tufts.vue.gui.GUI.getSystemIconForExtension(ext, size); if (image != null) { if (image.getWidth(null) > max) { // see http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html // on how to do this better/faster -- happens very rarely on the Mac tho, but need to test PC. Log.warn("scaling for dataType [" + ext + "]: " + Util.tags(image) + "; from " + image.getWidth(null) + "x" + image.getHeight(null)); image = image.getScaledInstance(max, max, 0); //Image.SCALE_SMOOTH); } return new javax.swing.ImageIcon(image); } else return null; } /** @return the image for our large icon if we have one, null otherwise */ public Image getLargeIconImage() { if (mLargeIcon == null) getLargeIcon(); if (mLargeIcon != null) return mLargeIcon.getImage(); else return null; } public Image getTinyIconImage() { if (mTinyIcon == null) getTinyIcon(); if (mTinyIcon != null) return mTinyIcon.getImage(); else return null; } /** @return an image to use when dragging this resource */ public Image getDragImage() { Image image = null; if (!isLocalFile()) { Icon icon = getContentIcon(); if (icon instanceof ResourceIcon) image = ((ResourceIcon)icon).getImage(); } if (image == null) image = getLargeIconImage(); return image; } private tufts.vue.ui.ResourceIcon mIcon; /** * @param repainter -- the component to request repainting on when * the icons loads if it's not immediately available. This is required * to support cell rendererers -- e.g., the component the icon paints * on does not forward repaint requests. May be null if cell renderers not in use. */ public synchronized javax.swing.Icon getContentIcon(java.awt.Component repainter) { //if (!isImage()) // return null; if (mIcon == null) { //tufts.Util.printStackTrace("getIcon " + this); System.exit(-1); // TODO: cannot cache this icon if there is a freakin painter, // (because we'd only remember the last painter, and prior // users of this icon would stop getting updates) // -- this is why putting a client property in the cell renderer // is key, tho it's annoying it will have to be fetched // every time -- or could create an interface: Repaintable mIcon = new tufts.vue.ui.ResourceIcon(this, 32, 32, repainter); } return mIcon; } public javax.swing.Icon getContentIcon() { return getContentIcon(null); } /** * Get preview of the object such as thummbnail / small sized image. Suggested return * types are something that can be converted to image data, or a java GUI component, * such as a java.awt.Component, javax.swing.JComponent, or javax.swing.Icon. */ public abstract Object getPreview(); /** * @return true if the data for this resource is normally obtained by making use the * the local file system (including attached network shares) * * This default impl always returns false. */ public boolean isLocalFile() { return false; } public boolean isPackaged() { return hasProperty(PACKAGE_FILE); } // public abstract java.io.InputStream getByteStream(); // public abstract void setCacheFile(java.io.File cacheFile); // public abstract java.io.File getCacheFile(); /** if this resource is relative to the given root, record this in the resource in a persistant way */ public abstract void recordRelativeTo(URI root); //public abstract void updateIfRelativeTo(URI root); /** if this resource was relative when it was persisted, see if it can be found relative to the new location */ public abstract void restoreRelativeTo(URI root); // /** @eprecated - if possible, make this Resource relatve to the given root */ // public abstract void makeRelativeTo(URI root); // /** @eprecated -- cleanup / remove */ // public void updateRootLocation(URI oldRoot, URI newRoot) {} // //public abstract void updateRootLocation(URI oldRoot, URI newRoot); /** * Return tooltip information, if any. Basic HTML tags are permitted. */ public String getToolTipText() { return toString(); } protected String paramString() { return ""; } private static final String _fmt0 = "%s@%07x[%s; %s%c%s]"; private static final String _fmt1 = "%s@%08x[%s; %s%c%s]"; private static final String _debugFmt = Util.getJavaVersion() > 1.5 ? _fmt1 : _fmt0; public String asDebug() { return String.format(_debugFmt, getClass().getSimpleName(), System.identityHashCode(this), TYPE_NAMES[getClientType()], paramString(), mDataFile == null ? 'S' : 'F', mDataFile == null ? Util.tags(getSpec()) : (Util.TERM_RED + mDataFile + Util.TERM_CLEAR) //getLocationName() // may trigger property fetches during debug which is very messy //(mDataFile != null && hasProperty(PACKAGE_FILE)) ? mDataFile.getName() : getSpec() ); } public String getLocationName() { return getSpec(); } public String getDescription() { return getSpec(); } @Override public String toString() { return asDebug(); } @Override public Resource clone() { try { final Resource clone = (Resource) super.clone(); clone.mProperties = mProperties.clone(); return clone; } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } } protected void out(String s) { Log.debug(String.format("%s@%08x: %s", getClass().getSimpleName(), System.identityHashCode(this), s)); } protected void out_info(String s) { Log.info(String.format("%s@%08x: %s", getClass().getSimpleName(), System.identityHashCode(this), s)); } protected void out_warn(String s) { Log.warn(String.format("%s@%08x: %s", getClass().getSimpleName(), System.identityHashCode(this), s)); } protected void out_error(String s) { Log.error(String.format("%s@%08x: %s", getClass().getSimpleName(), System.identityHashCode(this), s)); } /** @return true if the given path or filename looks like it probably contains image data in a format we understand * This just looks for common extentions (e.g., .gif, .jpg, etc). This can be applied to filenames, full paths, URL's, etc. */ public static boolean looksLikeImageFile(String path) { if (DEBUG.WORK) Log.debug("looksLikeImageFile [" + path + "]"); if (path == null || path.length() == 0) return false; String s = path.toLowerCase(); if (s.endsWith(".gif") || s.endsWith(".jpg") || s.endsWith(".jpe") || s.endsWith(".jpeg") || s.endsWith(".png") || s.endsWith(".tif") || s.endsWith(".tiff") || s.endsWith(".fpx") || s.endsWith(".bmp") || s.endsWith(".ico") ) return true; return false; } //public static boolean isLikelyURLorFile(String s) { public static boolean looksLikeURLorFile(String s) { if (s == null || s.length() == 0) return false; final char c0 = s.charAt(0); final char c1 = s.length() > 1 ? s.charAt(1) : 0; return c0 == '/' || c0 == '\\' || (Character.isLetter(c0) && c1 == ':') // Windows style C:\path\file || s.startsWith(java.io.File.separator) || s.startsWith("http://") || s.startsWith("file:") ; } /** * @return true if the given string looks like it MAY represent a file on the local file system, * such that the given string would successfully init a java.io.File object (even if the file doesn't exist) */ public static boolean looksLikeLocalFilePath(String s) { if (s == null) return false; final char c0 = s.length() > 0 ? s.charAt(0) : 0; if (Util.isWindowsPlatform()) { final char c1 = s.length() > 1 ? s.charAt(1) : 0; return c0 == '/' || c0 == '\\' || (Character.isLetter(c0) && c1 == ':') // Windows style C:\path\file || s.startsWith(java.io.File.separator) //|| s.startsWith("file:") ; } else { return c0 == '/' || s.startsWith(java.io.File.separator) //|| s.startsWith("file:") ; } } public static String toCanonical(File file) { String canonical = null; try { canonical = file.getCanonicalPath(); } catch (Throwable t) { Log.warn(file, t); } return canonical == null ? file.getAbsolutePath() : canonical; } public static File toCanonicalFile(File file) { String canonical = toCanonical(file); if (file.getPath().equals(canonical)) return file; else return new File(canonical); } /** @return a File object if one can be found (and if it exists) */ public static File getLocalFileIfPresent(String urlOrPath) { // todo: make semantics determinisitic: should this only return files // that already exist or not? File file = null; if (urlOrPath.startsWith("#")) { return null; // RDF code sometimes hands us these } else if (urlOrPath.startsWith("file:")) { file = new File(urlOrPath.substring(5)); } else if (looksLikeLocalFilePath(urlOrPath)) { file = new File(urlOrPath); } //if (DEBUG.IO && file != null) Log.debug("getLocalFileIfPresent(Str): testing " + file); //if (file == null || !file.exists()) // file = getLocalFileIfPresent(makeURL(urlOrPath)); if (file == null) { file = getLocalFileIfPresent(makeURL(urlOrPath)); } else { if (DEBUG.IO) Log.debug(Util.tags(file) + "; GLFIP testing"); if (!file.exists()) file = getLocalFileIfPresent(makeURL(urlOrPath)); } return file; } private static final String DEFAULT_ENCODING = "DEFAULT"; private static final String[] Encodings = { DEFAULT_ENCODING, "UTF-8", "MacRoman", "windows-1252", "ISO-8859-1" }; // this will only return files that already exist public static File getLocalFileIfPresent(URL url) { if (url == null || !"file".equals(url.getProtocol())) return null; if (DEBUG.RESOURCE) dumpURL(url, "getLocalFileIfPresent; from:"); File file = null; if (false) { // // Sometimes Win32 C:/foo/bar.jpg URI's will wind up with the entire path in // // the scheme-specifc part, not the path, which will be null, so we have // // nothing to create the file from. We could pull the scheme-specific if // // path is empty if we need to, but for now we're going to try woring with // // pure URL paths... // final URI uri = makeURI(url); // if (uri == null) // return null; // if (DEBUG.RESOURCE) dumpURI(uri, "made URI from " + Util.tags(url)); // try { // file = new File(uri.getPath()); // if (!file.exists()) // throw new RuntimeException("doesn't exist: " + file); // //file = new File(uri); // } catch (Throwable t) { // Log.warn("failed to create File from URI " + uri, t); // dumpURIError(uri, "unable to convert 'file:' URL"); // } } else { // The advantage of URI over URL here is that for paths such as // //.host/foo/bar.jpg, ".host" winds up in the URL authority, and the path // doesn't contain it, so we can't create a proper File without knowing how // to properly prefix the authorty with "//" or however many slashes may be // appropriate, and combine with the path using another '/', wheras at least // with the URN, the entire thing winds up together in the scheme-specifc // part. // // For example, this is from dumpURL on WinXP: (unlisted URL fields are // empty) This example is from a VMWare XP client connecting back to the // host. Similar may apply to network Win32 shares, tho the host will // probably start with a regular alphanumeric, instead of '.', in which case // everything might automatically wind up in the path -- need to test this. // // URL: file://.host/Shared Folders/Images/asciifull.gif // protocol: file // authority: .host // host: .host // path: /Shared Folders/Images/asciifull.gif // file: /Shared Folders/Images/asciifull.gif try { if (url.getAuthority() != null) { String fullPath = url.toString(); if (!fullPath.startsWith("file:")) throw new IllegalStateException("URL should already have had a file: protocol; " + url); fullPath = fullPath.substring(5); file = new File(fullPath); } else { file = new File(url.getPath()); } if (DEBUG.RESOURCE && file != null) dumpFile(file); if (!file.isAbsolute()) { // We could handle checking for relative files (relative to the map) // if we had a ref to the ResourceFactory, and we added a method // there for finding files relative to the map. if (DEBUG.Enabled) Log.debug("ignoring non-absolute: " + Util.tags(file)); return null; } if (DEBUG.IO) Log.debug(Util.tags(file) + "; getLocalFileIfPresent(URL): testing"); if (!file.exists()) { boolean exists = false; final String fullpath = file.toString(); if (fullpath.indexOf('%') >= 0) { // TODO: keep repeating this until no more % or no change after decoding Log.info(Util.tags(file) + "; claims non-existent, attempting decode:"); for (String encoding : Encodings) { String decoded = "<failed>"; try { if (encoding == DEFAULT_ENCODING) decoded = java.net.URLDecoder.decode(fullpath); else decoded = java.net.URLDecoder.decode(fullpath, encoding); file = new File(decoded); } catch (Throwable t) { Log.error("decoding failure on " + Util.tags(fullpath) + " as " + encoding, t); continue; } Log.info(String.format("Decoded to %12s: ", encoding) + Util.tags(decoded)); if (exists = file.exists()) { Log.info(Util.tags(file) + "; file findable after decoding"); break; } } if (!exists) Log.info(Util.tags(file) + "; cannot find under any decoding."); } if (!exists) { Log.info(Util.tags(file) + "; ignoring non-existent"); return null; } } //if (DEBUG.Enabled) Log.debug("got canonical path: " + file.getCanonicalPath()); //if (!file.exists()) throw new RuntimeException("doesn't exist: " + file); } catch (Throwable t) { Log.warn("failed to create File from URL " + url, t); dumpURLError(url, "unable to convert 'file:' URL"); } } //if (DEBUG.RESOURCE) Log.debug("got File from URL: " + Util.tags(file) + "; from " + Util.tags(url)); return file; } protected static String encodeForURL(String s) //throws java.io.UnsupportedEncodingException { //String encoded; try { //encoded = java.net.URLEncoder.encode(s, "UTF-8"); return java.net.URLEncoder.encode(s, "UTF-8"); } catch (Throwable t) { Log.error("Failed to encode [" + s + "]", t); return java.net.URLEncoder.encode(s); } //return encoded; } // // public static String URLEncode(URI uri) // // } protected static String decodeURI(String s) throws java.io.UnsupportedEncodingException { String decoded = java.net.URLDecoder.decode(s, "UTF-8"); return decoded; } protected static String decodeForURL(String s) throws java.io.UnsupportedEncodingException { return decodeURI(s); } private static String encodeForURI(String s) { //if (true) return s; s = s.replaceAll(" ", "%20"); if (s.indexOf('\\') >= 0 && !Util.isWindowsPlatform()) { //if (DEBUG.RESOURCE) Log.debug("reversing slashes in " + s); Log.warn("reversing slashes in " + s); // this is of marginal usefulness -- the source URI was presumably created // on another platform (and thus machine), referring to a resource we // undoubtably won't be able to access, but at least this lets us consistently // create URI's. s = s.replace('\\', '/'); } return s; //return s.replace(' ', '+'); // no good } protected static String decodeForFile(String s) { String decoded = s; try { decoded = decodeURI(s); } catch (Throwable t) { Log.warn("decodeForFile: " + t + "; " + s); } return decoded; } protected static File toFile(URI uri) { return new File(uri.getPath()); } /** @return a URL for the given file, otherwise null */ public static java.net.URL makeURL(final File file) { try { return file.toURL(); } catch (Throwable t) { Log.warn(Util.tags(file) + " failed to convert itself to a URL", new Throwable()); return makeURL(file.toString()); } } private static final String URL_FILE_PROTOCOL_PREFIX = "file://"; /** If given string is a valid URL, make one and return it, otherwise, return null. * * @return the new URL -- returned URL's will be fully decoded * @see java.net.URLDecoder * **/ public static java.net.URL makeURL(String s) { try { if (s != null && s.startsWith("feed:")) s = "http:" + s.substring(5); URL url = null; try { url = new URL(s); } catch (java.net.MalformedURLException e) { if (DEBUG.WORK || DEBUG.RESOURCE) Log.info("makeURL: " + s + "; " + e); } if (url != null) return url; final URI uri = makeURI(s); String decoded = null; try { //decoded = URLDecode(uri.toString()); decoded = java.net.URLDecoder.decode(uri.toString(), "UTF-8"); //decoded = replaceAll("+", url = new URL(decoded); } catch (Throwable t) { String msg = "couldn't make URL from decoded URI: " + Util.tags(decoded == null ? uri : decoded); if (DEBUG.Enabled) Log.info(msg, t); else Log.info(msg + "; " + t); //if (decoded != null && !decoded.equals(uri.toString()) // URI.toURL leaves the URL in encoded form: local file paths need decoding to be useful to java.io.File url = uri.toURL(); } //final URL url = uri.toURL(); //final URL url = new URL(java.net.URLDecoder.decode(uri.toString(), "UTF-8")); //if (DEBUG.RESOURCE && url != null) dumpURL(url, "MADE URL FROM " + Util.tags(s) + "; via " + Util.tags(uri)); if ((DEBUG.WORK || DEBUG.RESOURCE) && url != null) dumpURL(url, "Made URL FROM " + Util.tags(uri)); return url; } catch (Throwable t) { Log.warn("couldn't make URL from: " + Util.tags(s) + "; " + t); return null; } } public static URI makeURI(String s) { final char c0 = s.length() > 0 ? s.charAt(0) : 0; final char c1 = s.length() > 1 ? s.charAt(1) : 0; final String txt; URI uri = null; try { if (c0 == '#' || s.startsWith("rdf:#")) { // Our current RDF code sometimes tries to make Resources from random // non string fragments, which will always fail to create a URI unless // both a URI scheme and a URI scheme-specific-part are also specified. final String schemeSpecific = "fragment"; if (c0 == '#') uri = new java.net.URI("rdf", schemeSpecific, s.substring(1)); else uri = new java.net.URI("rdf", schemeSpecific, s.substring(5)); Log.warn("makeURI: guessed at creating " + Util.tags(uri) + " from " + Util.tags(s)); } else if (c0 == '/' || c0 == '\\' || (Character.isLetter(c0) && c1 == ':')) { // the above conditions test: // first case: MacOSX / Linux / Unix path // second case: Windows path // third case: Windows "C:" style path // This URI constructor will auto-encode (fully) the input string. // This will include, on unix platforms, encode windows '\' file // separators as "%5C". uri = new java.net.URI("file", s, null); // last argument is fragment: never needed for files //Util.printStackTrace("makeURI FILE:// -ified: " + txt); } else { final String encoded = encodeForURI(s); uri = new java.net.URI(encoded); if (uri != null) uri = uri.normalize(); } if (uri != null && uri.getScheme() == null) uri = new java.net.URI("file:" + uri.toString()); } catch (Throwable t) { Util.printStackTrace(t, "makeURI: " + Util.tags(s)); } if (DEBUG.RESOURCE) { if (uri != null) dumpURI(uri, "Made URI FROM " + Util.tags(s)); //if (uri != null) dumpURI(uri, " MADE FROM STRING: " + s); // if (uri != null && uri.toString().equals(s)) // System.err.println(" MADE URI: " + uri); // else // System.err.println(" MADE URI: " + uri + " src=[" + s + "]"); } return uri; } public static URI makeURI(URL url) { //URI uri = url.toURI(); // all this does is "new URI(toString())" final String encoded = encodeForURI(url.toString()); URI uri = null; try { uri = new URI(encoded); uri = uri.normalize(); } catch (Throwable t) { Log.debug("URI from " + Util.tags(url), t); dumpURL(url); } return uri; } // public static URI makeURI(File f) { // URI uri = f.toURI(); // if (DEBUG.RESOURCE) dumpURI(uri, "NEW FILE URI FROM " + f); // Util.printStackTrace("makeURI from " + Util.tags(f) + "; manually checking for /C:"); // // TODO: this "/C:" check isn't generic enough: is this code even being called anywhere? // if (uri.getPath().startsWith("/C:")) // return makeURI(uri.getPath().substring(3)); // else // return uri; // } protected static Object debugURI(String s) { try { return new URI(s); } catch (Throwable t) { return t; //return t.toString() + "; " + Util.tags(s); } } protected static String debugURL(String s) { try { return new URL(s).toString(); } catch (Throwable t) { return t.toString(); //return t.toString() + "; " + Util.tags(s); } } public static boolean canDump(Object o) { return o instanceof File || o instanceof URL || o instanceof URI; } public static String getDump(Object o) { return getDump(o, null); } /** @return a dump of either a URI, URL or File object */ public static String getDump(Object o, String msg) { final StringWriter buf = new StringWriter(256); final PrintWriter w = new PrintWriter(buf); if (msg != null) w.println(msg); if (o instanceof URI) writeURI(w, (URI) o, msg); else if (o instanceof URL) writeURL(w, (URL) o, msg); else if (o instanceof File) writeFile(w, (File) o, msg); else w.print("\tResource: unhandled getDump for object of type: " + Util.tags(o)); return buf.toString(); } private static void dumpOut(Object o, String msg, boolean toError) { if (msg == null) msg = Util.TERM_RED + "Made " + o.getClass().getName() + ";" + Util.TERM_CLEAR; final String txt = getDump(o, msg); if (toError) Log.error(txt); else Log.debug(txt); } public static void dumpURI(URI u, String msg, boolean error) { dumpOut(u, msg, error); } public static void dumpURI(URI u, String msg) { dumpURI(u, msg, false); } public static void dumpURIError(URI u, String msg) { dumpURI(u, msg, true); } public static void dumpURI(URI u) { dumpURI(u, null, false); } public static void dumpURL(URL u, String msg, boolean error) { dumpOut(u, msg, error); } public static void dumpURL(URL u, String msg) { dumpURL(u, msg, false); } public static void dumpURLError(URL u, String msg) { dumpURL(u, msg, true); } public static void dumpURL(URL u) { dumpURL(u, null); } public static void dumpFile(File u, String msg, boolean error) { dumpOut(u, msg, error); } public static void dumpFile(File u) { dumpFile(u, null, false); } // public static void dumpURI(URI u, String msg, boolean error) { // final StringWriter buf = new StringWriter(256); // final PrintWriter w = new PrintWriter(buf); // if (msg == null) msg = "Made URI;"; // } private static void writeURI(PrintWriter w, URI u, String msg) { w.printf("%20s: %s (@%x) %s %s", "URI", u, System.identityHashCode(u), u.isAbsolute() ? "ABSOLUTE" : "RELATIVE", u.isOpaque() ? "OPAQUE" : "" ); if (DEBUG.META) writeField(w, "hashCode", Integer.toHexString(u.hashCode())); writeField(w, "scheme", u.getScheme()); writeField(w, "scheme-specific", u.getSchemeSpecificPart(), u.getRawSchemeSpecificPart()); writeField(w, "authority", u.getAuthority(), u.getRawAuthority()); writeField(w, "userInfo", u.getUserInfo(), u.getRawUserInfo()); writeField(w, "host", u.getHost()); if (u.getPort() != -1) writeField(w, "port", u.getPort()); writeField(w, "path", u.getPath(), u.getRawPath()); writeField(w, "query", u.getQuery(), u.getRawQuery()); writeField(w, "fragment", u.getFragment(), u.getRawFragment()); } private static void writeFile(PrintWriter w, File u, String msg) { w.printf("%20s: %s (@%x)", "File", u, System.identityHashCode(u)); if (DEBUG.META) writeField(w, "hashCode", Integer.toHexString(u.hashCode())); writeField(w, "path", u.getPath()); writeField(w, "absolutePath", u.getAbsolutePath()); try { writeField(w, "canonicalPath", u.getCanonicalPath()); } catch (Throwable t) { t.printStackTrace(); } try { writeField(w, "toURI", u.toURI()); } catch (Throwable t) { t.printStackTrace(); } try { writeField(w, "toURL", u.toURL()); } catch (Throwable t) { t.printStackTrace(); } writeField(w, "name", u.getName()); writeField(w, "parent", u.getParent()); writeField(w, "isAbsolute", u.isAbsolute()); writeField(w, "isNormalFile", u.isFile()); writeField(w, "isDirectory", u.isDirectory()); writeField(w, "exists", u.exists()); writeField(w, "canRead", u.canRead()); } private static void writeURL(PrintWriter w, URL u, String msg) { w.printf("%20s: %s (@%x)", "URL", u, System.identityHashCode(u)); if (DEBUG.META) writeField(w, "hashCode", Integer.toHexString(u.hashCode())); writeField(w, "protocol", u.getProtocol()); writeField(w, "userInfo", u.getUserInfo()); writeField(w, "authority", u.getAuthority()); writeField(w, "host", u.getHost()); if (u.getPort() != -1) writeField(w, "port", u.getPort()); writeField(w, "path", u.getPath()); writeField(w, "file", u.getFile()); writeField(w, "query", u.getQuery()); writeField(w, "ref", u.getRef()); } private static void writeField(PrintWriter w, String label, Object value) { if (value != null && !(value instanceof String && value.toString().length() == 0)) w.printf("\n%20s: %s", label, value); //System.out.format("%20s: %s\n", label, value); } private static void writeField(PrintWriter w, String label, Object value, Object rawValue) { writeField(w, label, value); if (value != null && !value.equals(rawValue) || rawValue == null && value != null) writeField(w, "RAW-" +label, rawValue); } } // add getSource (or: get Location/Repository/Collection/DataSource/Where) (e.g. "ArtStor", "Internet/Web", "Local File") // may be a good enough replacement for type? type currently mixes location with content // a bit by adding "directory", which along with "file" are really just both "Local File" locations, // and "Favorites" is a total odd man-out: any given resource that happens to be in the // favorites list may actually be of any of the other given types). // get rid of spec, but maybe include a getPath, and include a hasURL/getURL as is too damn handy. // add getReader, getContent (need something to get an image object, or do we handle that // via a cross-cutting concern?) // get rid of getExtension, tho we may be trying to get a mime-type with that one. // it *would* be really nice if we could easily know if this is, say, an image or HTML content tho // maybe we do add mime-type and punt with "type/unknown" for most everything else // until we might someday be able to extract this info from the filesystem. Still // might be useful enough to add a special case "isImage" tho (or maybe hasImage?) // // get rid of getToolTipInformation: getTitle will be our only special case // mapping of information from either meta-data or the URL/file-name to something // exposed in the API. // // Maybe even get rid of displayContent: make a cross-cutting concern handled // by a ResourceHandler? could theoretically put stuff like isImage/isHTML // there also, altho we could leave both of this in the API and address // the weight of it by having a good AbstractResource that handles all // this stuff for you. // // Oh, we'll still need something for persistance tho, which spec was handling. // Maybe getAddress? getUniqueID (a GLOBAL id, which implies us coming up // with protocol names for every OSID resource, but we had that problem // anyway. Hmm: we could actually use the class name of the OSID DR impl // and let that handle it? (or really the DataSource, unless we get // rid of that). // // Maybe get rid of getPreview for now; may add in a set of getPreview / getViewer / getEditor later. // // getIcon: make getThumbnail? this needs to be defined: is too generic: maybe get rid of. // the Asset/DataSource logo should come from the DataSource. So maybe we need // a getDataSource, which is really what the getWhere ends up being, and returns // an object instead of a name. Tho is of course handy for not just thumbnails, // but say type-based application icons (from OS meta-data), or say the favicon.ico // from a website. // // AND: if we ever get int getting "parts", maybe we should throw this whole thing out // use the DR API itself? a bit heavy weight tho. // // And don't forget: altho we want to handle "any kind of data source", the majority // of them are probably all going to be URL based, at least for the forseeabe future, // which is a practical consideration worth keeping in mind. // So the basic services being provided are: // 1 - get data for VUE (provide a handle for it) // 2 - get meta-data about the data (including the all important where this thing came from) // 3 - provide persistance of the reference to the data for VUE // 4 - provide some convenince wrapping of meta-data to tell us some very // useful things such as: // - a title // - a URL if available // - what we can do with this data (it's type), such is isImage, isHTML, and/or getMimeType // // Items 1-3 are an absolute requiment. Items in 4 are for things useful // enough to include in the API, but only priority items worth expanding // the API for (these items are all computed from the meta-data, both properties based and DataSource based) // // Could also consider calling this an "Asset", tho that may be just too confusing given OKI & Fedora. // // Possibly add a "getName", to be the raw stub of a URL v.s. a massaged one for getTitle, // or former v.s. the meta-data title of a properly title document/image from a repository. // This is pure convience for a GUI / possibly "important" data for a user. Maybe only // really for local files tho. Could opt out of it but putting it in title and just // not having a massaged version that leaves (that does stuff like create spaces, removes // the extension, etc).