/* * 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 java.util.*; import tufts.Util; import static tufts.Util.*; import tufts.vue.gui.GUI; import java.net.*; import java.awt.Image; import java.io.*; import java.util.regex.*; /** * The Resource impl handles references to local files or single URL's, as well as * any underlying type of asset (OSID or not) that can obtain it's various parts via URL's. * * An "asset" is defined very generically as anything, that given some kind basic * key/meta-data (e.g., a file name, a URL, etc), can at some later point, * reliably and repeatably convert that name/key to underlyling data of interest. * This is basically what the Resource interface was created to handle. * * An "Asset" is a proper org.osid.repository.Asset. * * When this class is used for an asset with parts (e..g, Osid2AssetResource), it should * also be what allows us to completely throw away any underlying * org.osid.repository.Asset (using it only as a paramatizer for what is really a * factory constructor: should covert to that), because all the assets can be had via * URL's, and we've extracted the relvant information at construction time. If the * asset part(s) CANNOT be accessed via URL, then we need a real, new subclass of * URLResource that handles the non URL cases, or even just a raw implementor of * Resource, if all the asset-parts need special I/O (e.g., non HTTP network traffic), * to be obtained. * * @version $Revision: 1.91 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $ */ public class URLResource extends Resource implements XMLUnmarshalListener { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(URLResource.class); //private static final String BROWSE_KEY = "@Browse"; private static final String IMAGE_KEY = HIDDEN_PREFIX + "Image"; private static final String THUMB_KEY = HIDDEN_PREFIX + "Thumb"; private static final String USER_URL = "URL"; private static final String USER_FILE = "File"; private static final String USER_DIRECTORY = "Directory"; private static final String USER_FULL_FILE = RUNTIME_PREFIX + "Full File"; private static final String FILE_RELATIVE = HIDDEN_PREFIX + "file.relative"; private static final String FILE_RELATIVE_OLD = "file.relative"; private static final String FILE_CANONICAL = HIDDEN_PREFIX + "file.canonical"; /** * The most generic version of what we refer to. Usually set to a full URL or absolute file path. * Note that this may have been set on a different platform that we're currently running on, * so it may no longer make a valid URL or File on this platform, which is why we need * this generic String version of it, and why the Resource/URLResource code can be so complicated. */ private String spec = SPEC_UNSET; /** * A default URL for this resource. This will be used for "browse" actions, so for * example, it may point to any content available through a URL: an HTML page, raw image data, * document files, etc. */ private URL mURL; /** Points to raw image data (greatest resolution available) */ private URL mURL_ImageData; /** Points to raw image data for an image thumbnail */ private URL mURL_ThumbData; /** * This will be set if we point to a local file the user has control over. * This will not be set to point to cache files or package files. */ private File mFile; /** * If this resource is relative to it's map, this will be set (at least by the time we're persisted) */ private URI mRelativeURI; /** an optional resource title */ private String mTitle; private boolean mRestoreUnderway = false; private ArrayList<PropertyEntry> mXMLpropertyList; static URLResource create(String spec) { return new URLResource(spec); } static URLResource create(URL url) { return new URLResource(url.toString()); } static URLResource create(URI uri) { return new URLResource(uri.toString()); } static URLResource create(File file) { return new URLResource(file); } private URLResource(String spec) { init(); setSpec(spec); } private URLResource(File file) { init(); setSpecByFile(file); } /** * @deprecated - This constructor needs to be public to support castor persistance ONLY -- it should not * be called directly by any code. */ public URLResource() { init(); } private void init() { //if (DEBUG.RESOURCE || DEBUG.DR) { if (DEBUG.RESOURCE) { //out("init"); String iname = getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(this)); //tufts.Util.printStackTrace("INIT " + iname); setDebugProperty("0INSTANCE", iname); } } // @Override // public String getContentType() { // if (mURL_Default != null) // return extractExtension(mURL_Default); // else // return super.getContentType(); // } // todo: rename relativeName, and add a "shortName", for what CabinetResource provides // (which will also translate ':' to '/' on the mac) private static final String FILE_DIRECTORY = "directory"; private static final String FILE_NORMAL = "file"; private static final String FILE_UNKNOWN = "unknown"; protected void setSpecByKnownFile(File file, boolean isDir) { setSpecByFile(file, isDir ? FILE_DIRECTORY : FILE_NORMAL); } protected void setSpecByFile(File file) { setSpecByFile(file, FILE_UNKNOWN); } private void setSpecByFile(File file, Object knownType) { if (file == null) { Log.error("setSpecByFile", new IllegalArgumentException("null java.io.File")); return; } if (DEBUG.RESOURCE) dumpField("setSpecByFile; type=" + knownType, file); //if (DEBUG.RESOURCE && DEBUG.META) dumpField("setSpecByFile; type=" + knownType, file); if (mURL != null) mURL = null; // if (knownType == FILE_UNKNOWN) { // // This works on XP and Vista as of at least Java6 for standard file links // // (.lnk files), and recognizes .url's as links (isLink()=true), but .url's // // link locations are always null. None of this appears to work on all on // // the Mac, tho I've only tested Java5 there (Java6 not production release // // yet) // try { // ShellFolder sf = ShellFolder.getShellFolder(file); // if (sf.isLink()) // Util.printStackTrace("GOT LINK: " + file + " --> " + sf.getLinkLocation()); // } catch (Throwable t) { // t.printStackTrace(); // } // } setFile(file, knownType); String fileSpec = null; try { //fileSpec = file.getCanonicalPath(); // may actually not be friendly to volume paths fileSpec = file.getPath(); } catch (Throwable t) { // for IOException Log.warn(file, t); fileSpec = file.getPath(); } setSpec(fileSpec); if (DEBUG.RESOURCE && DEBUG.META && "/".equals(fileSpec)) { Util.printStackTrace("Root FileSystem Resource created from: " + Util.tags(file)); } } private long mLastModified; private void setFile(File file, Object type) { if (mFile == file) return; if (DEBUG.RESOURCE||file==null) dumpField("setFile", file); mFile = file; if (file == null) return; if (mURL != null) setURL(null); type = setDataFile(file, type); if (mTitle == null) { // still true?: for some reason, if we don't always have a title set, tooltips break. SMF 2008-04-13 String name = file.getName(); if (name.length() == 0) { // Files that are the root of a filesystem, such "C:\" will have an empty name // (Presumably also true for "/") setTitle(file.toString()); } else { if (Util.isMacPlatform()) { // colons in file names on Mac OS X display as '/' in the Finder name = name.replace(':', '/'); } setTitle(name); } } if (type == FILE_DIRECTORY) { setClientType(Resource.DIRECTORY); } else if (type == FILE_NORMAL) { setClientType(Resource.FILE); if (DEBUG.IO) dumpField("scanning mFile", file); mLastModified = file.lastModified(); setByteSize(file.length()); // todo: could attempt setURL(file.toURL()), but might fail for Win32 C: paths on the mac if (DEBUG.RESOURCE) { setDebugProperty("file.instance", mFile); setDebugProperty("file.modified", new Date(mLastModified)); // if (true) { // setDebugProperty("file.toURI", mFile.toURI()); // try { // setDebugProperty("file.toURL", mFile.toURL()); // } catch (Throwable t) { // setDebugProperty("file.toURL", t.toString()); // } // } } } } /** * Set the local file that refers to this resource, if there is one. * If mFile is set, mDataFile will always to same. If this is a packaged * resource, mFile will NOT be set, but mDataFile should be set to the package file */ private Object setDataFile(File file, Object type) { // TODO performance: can skip isDirectory and exists tests if we // know this came from a LocalCabinet, which may speed up that // dog-slow code when expanding big directories. if (type == FILE_DIRECTORY || (type == FILE_UNKNOWN && file.isDirectory())) { if (DEBUG.RESOURCE && DEBUG.META) out("setDataFile: ignoring directory: " + file); //Log.warn("directory as data-file: " + file, new Throwable()); //if (DEBUG.RESOURCE) out("no use for directory data files"); return FILE_DIRECTORY; } final String path = file.toString(); if (path.length() == 3 && Character.isLetter(path.charAt(0)) && path.endsWith(":\\")) { // Check for A:\, etc. // special case to ignore / prevent testing Windows currently in-accessable mount points // File.exists may take a while to time-out on these. if (DEBUG.Enabled) out_info("setDataFile: ignoring Win mount: " + file); return FILE_DIRECTORY; } if (type == FILE_UNKNOWN) { if (DEBUG.IO) out("testing " + file); if (!file.exists()) { // todo: could attempt decodings if a '%' is present // todo: if any SPECIAL chars present, could attempt encoding in all formats and then DECODING to at least the platform format out_warn(TERM_RED + "no such active data file: " + file + TERM_CLEAR); //Util.printStackTrace("HERE"); //throw new IllegalStateException(this + "; no such active data file: " + file); return FILE_UNKNOWN; } } mDataFile = file; if (mDataFile != mFile) { if (DEBUG.IO) dumpField("scanning mDataFile ", mDataFile); setByteSize(mDataFile.length()); mLastModified = mDataFile.lastModified(); } if (DEBUG.RESOURCE) { dumpField("setDataFile", file); setDebugProperty("file.data", file); } return FILE_NORMAL; } /** for use by tufts.vue.action.Archive */ public void setPackageFile(File packageFile, File archiveFile) { if (DEBUG.RESOURCE) dumpField("setPackageFile", packageFile); reset(); setURL(null); setFile(null, FILE_UNKNOWN); setProperty(PACKAGE_FILE, packageFile); removeProperty(USER_FILE); // don't want to see the File // if (!hasProperty("Title")) { // if (mTitle != null) // setRuntimeProperty("Title", mTitle); // // else // // setRuntimeProperty("Title", packageFile.getName()); // } setProperty(PACKAGE_ARCHIVE, archiveFile); setCached(true); } @Override public void reset() { super.reset(); invalidateToolTip(); } public final void XML_setSpec(final String XMLspec) { if (DEBUG.RESOURCE) dumpField("XML_setSpec", XMLspec); this.spec = XMLspec; } public void setSpec(final String newSpec) { if ((DEBUG.RESOURCE||DEBUG.WORK) && this.spec != SPEC_UNSET) { out("setSpec; already set: replacing " + Util.tags(this.spec) + " " + Util.tag(spec) + " with " + Util.tags(newSpec) + " " + Util.tag(newSpec)); //Log.warn(this + "; setSpec multiple calls", new IllegalStateException("setSpec: multiple calls; resources are atomic")); //return; } if (DEBUG.RESOURCE) dumpField(TERM_CYAN + "setSpec------------------------" + TERM_CLEAR, newSpec); if (newSpec == null) throw new IllegalArgumentException(Util.tags(this) + "; setSpec: null value"); if (SPEC_UNSET.equals(newSpec)) { this.spec = SPEC_UNSET; return; } this.spec = newSpec; reset(); if (!mRestoreUnderway) parseAndInit(); //if (DEBUG.RESOURCE) out("setSpec: complete; " + this); } public void XML_completed(Object context) { mRestoreUnderway = false; // If this Resource is relative and is going to be changing, we'd actually // rather NOT run final init now -- we'd really like to wait for the LWMap to do // it's relatvizing... This is the purpose of adding the "context" argument -- // we can now check the context object -- if it's the new default of // MANAGED_MARSHALLING, we don't initialize the resource yet -- we allow init to // be delayed to code in places such as Archive or LWMap can tweak them before // their final init. if (context != MANAGED_UNMARSHALLING) { if (DEBUG.RESOURCE && DEBUG.META) out("XML_completed: unmanaged (immediate) init in context " + Util.tags(context) + "; " + this); //if (DEBUG.Enabled) Log.info("XML_completed: unmanaged (immediate) finalInit in context " + Util.tags(context) + "; " + this); initAfterDeserialize(context); initFinal(context); if (DEBUG.RESOURCE) out("XML_completed"); } else { if (DEBUG.RESOURCE && DEBUG.META) out("XML_completed; delayed init"); } //if (DEBUG.CASTOR || DEBUG.RESOURCE) out("XML_completed: END"); } @Override protected void initAfterDeserialize(Object context) { loadXMLProperties(); } @Override protected void initFinal(Object context) { if (DEBUG.RESOURCE) out("initFinal in " + context); parseAndInit(); } private void loadXMLProperties() { if (mXMLpropertyList == null) return; for (KVEntry entry : mXMLpropertyList) { String key = (String) entry.getKey(); final Object value = entry.getValue(); // TODO: for older property maps (how to tell?) we want to re-sort the keys... // (and possible collapse the old keyname.### uniqified key names) // Todo: detect via content inspection: if contains a URL or Title, and they're // not at the top, do a sort. if (DEBUG.Enabled) { // todo: just check for keyname.###$ pattern, and somehow annotate new // MetaMaps so we only do this for the old ones final String lowKey = key.toLowerCase(); //Log.debug("inspecting key [" + lowKey + "]"); if (lowKey.startsWith("subject.")) key = "Subject"; else if (lowKey.startsWith("keywords.")) key = "Keywords"; } try { // probably faster to do single set of hashed lookups at end: if (IMAGE_KEY.equals(key)) { if (DEBUG.RESOURCE) dumpField("processing key", key); setURL_Image((String) value); } else if (THUMB_KEY.equals(key)) { if (DEBUG.RESOURCE) dumpField("processing key", key); setURL_Thumb((String) value); } else { //setProperty(key, value); addProperty(key, value); } } catch (Throwable t) { Log.error(this + "; loadXMLProperties: " + Util.tags(mXMLpropertyList), t); } } mXMLpropertyList = null; } private void setURL(URL url) { if (mURL == url) return; mURL = url; if (DEBUG.RESOURCE) { dumpField("setURL", url); setDebugProperty("URL", mURL); } if (url == null) return; if (mFile != null) setFile(null, FILE_UNKNOWN); } @Override protected String extractExtension() { if (mURL != null) return super.extractExtension(mURL.getPath()); else return super.extractExtension(); } //----------------------------------------------------------------------------- // Todo Someday: If possible, try and take into account lazy eval so we don't // actually have to create a File object, see if it fails to be a valid path, and // then test File.exists for every possible file object (may slow down // CabinetResource quite a bit). // // We DO need to handle initing a resource as a file resource from a missing // file, that may re-appear. Plus, if it is a relative reference, it may need // re-writing by LWMap. //----------------------------------------------------------------------------- private void parseAndInit() { //if (DEBUG.RESOURCE) out("parseAndInit"); if (spec == SPEC_UNSET) { Log.error(new Throwable("cannot initialize resource " + Util.tags(this) + " without a spec: " + Util.tags(spec))); return; } //if (DEBUG.RESOURCE) out("parseAndInit, mURL=" + mURL + "; mFile=" + mFile); if (isPackaged()) { setDataFile((File) getPropertyValue(PACKAGE_FILE), FILE_UNKNOWN); if (mFile != null) Log.warn("mFile != null" + this, new IllegalStateException(toString())); } else if (mFile == null && mURL == null) { File file = getLocalFileIfPresent(spec); if (file != null) { setFile(file, FILE_UNKNOWN); // actually, getLocalFileIfPresent may already know this exists (would need new type: FILE_KNOWN) } else { URL url = makeURL(spec); // a random string spec will not be a existing File, but will default to // create a file:RandomString URL (e.g. "file:My Computer"), so only set // URL here if it's a non-file: if (url != null && !"file".equals(url.getProtocol())) setURL(url); } } if (getClientType() == Resource.NONE) { if (isLocalFile()) { if (mFile != null && mFile.isDirectory()) setClientType(Resource.DIRECTORY); else setClientType(Resource.FILE); } else if (mURL != null) setClientType(Resource.URL); } if (getClientType() != Resource.DIRECTORY && !isImage()) { // once an image, always an image (cause setURL_Image may be called before setURL_Browse) if (mFile != null) setAsImage(looksLikeImageFile(mFile.getName())); // this just a minor optimization else setAsImage(looksLikeImageFile(this.spec)); // this is the default if (!isImage()) { // double-check the meta-data in case looksLikeImageFile didn't give us 100% accurate results checkForImageType(); } } //----------------------------------------------------------------------------- // Set property information, mainly for the user, that will display // the minimum of what/where the resource is. //----------------------------------------------------------------------------- if (isLocalFile()) { if (mFile != null) { if (isRelative()) { //setProperty(USER_FILE, getRelativePath()); setProperty(USER_FULL_FILE, mFile); // handled in setRelativePath } else { setProperty(USER_FILE, mFile); // todo: later // if (getClientType() == DIRECTORY) { // removeProperty(USER_FILE); // setProperty(USER_DIRECTORY, mFile); // } else { // removeProperty(USER_DIRECTORY); // setProperty(USER_FILE, mFile); // } //--------------------------------------------------------------------------------------------------- // // TODO: WARNING: COMPUTING THE CANONICAL FILE IS VERY, VERY SLOW. // // TODO: Okay to do this on map save, but to for every damn resource -- // // E.g., this includes every instance of CabinetResource // final String canonical = toCanonical(mFile); // if (!mFile.getPath().equals(canonical)) { // setProperty(FILE_CANONICAL, canonical); // will persist // setProperty(USER_FULL_FILE, canonical); // // TODO: make this a persisted property, and use it as a // // backup in case the non-canonical file path (e.g., via a // // volume mount on Mac OS X) goes missing, but the absolute // // path is there. This could happen if the user renames // // their hard drive, changing the volume name, tho the path // // would still be the same. // } else { // // as may have been persisted, remove now just in case // //removeProperty(FILE_CANONICAL); // } //--------------------------------------------------------------------------------------------------- } } else { setProperty(USER_FILE, spec); } removeProperty(USER_URL); } else { // todo: can use some of our getLocalFileIfPresent code to determine if // this is a valid URL v.s. a File from an unfamiliar filesystem String proto = null; if (mURL != null) proto = mURL.getProtocol(); if (proto != null && (proto.startsWith("http") || proto.equals("ftp"))) { setProperty("URL", spec); removeProperty(USER_FILE); } else { if (DEBUG.RESOURCE) { if (!isPackaged()) { setDebugProperty("FileOrURL?", spec); setDebugProperty("URL.proto", proto); } } } } if (DEBUG.RESOURCE) { setDebugProperty("spec", spec); //setDebugProperty("Spec.0-encode", encodeForURL(spec)); //setDebugProperty("Spec.1-URI", debugURI(spec)); //setDebugProperty("Spec.2-VueURI", makeURI(spec)); if (mTitle != null) setDebugProperty("title", mTitle); } if (!hasProperty(CONTENT_TYPE) && mURL != null) setProperty(CONTENT_TYPE, java.net.URLConnection.guessContentTypeFromName(mURL.getPath())); if (DEBUG.RESOURCE) { out(TERM_GREEN + "final---" + this + TERM_CLEAR); //new Throwable("FYI").printStackTrace(); } } private void checkForImageType() { if (!isImage()) { if (hasProperty(CONTENT_TYPE)) { setAsImage(isImageMimeType(getProperty(CONTENT_TYPE))); } else { // TODO: on initial creation of resources with types unidentifiable from the spec, // this code will load CONTENT_TYPE (in getDataType), and determine isImage // with looksLikeImageFile, but then when saved/restored, the above case // will use isImageMimeType, which isn't the exact same test -- fix this. setAsImage(looksLikeImageFile('.' + getDataType())); /* if (!isImage()) { //boolean b =false; //try { // b= isJpegFile(getSpec()); //} catch (IOException e) { // TODO Auto-generated catch block // e.printStackTrace(); //} //b= isGifFile(getSpec()); //System.out.println("IS JPEG : " + b); //setAsImage(b); }*/ } } } /* Added these two methos isJPegFile and isGifFile that will look at the first 4 bytes of the file rather then use file extension to determine if a file is an image, this was needed to try out som LaTex stuff from from a forum discussion, it also may be handy to come back to if we want to use images generated by SEASR since those will be coming from a WS call and won't have the proper extension */ public boolean isJpegFile(String s) throws IOException { URI url; try { url = new URI(s); } catch (URISyntaxException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } URLConnection conn = url.toURL().openConnection(); InputStream in = new BufferedInputStream(conn.getInputStream()); try { return (in.read() == 'J' && in.read() == 'F' && in.read() == 'I' && in.read() == 'F'); } finally { try { in.close(); } catch (IOException ignore) {} } } public boolean isGifFile(String s) { URI url; try { url = new URI(s); } catch (URISyntaxException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } URLConnection conn = null; try { conn = url.toURL().openConnection(); } catch (MalformedURLException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } InputStream in = null; try { in = new BufferedInputStream(conn.getInputStream()); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } try { return (in.read() == 'G' && in.read() == 'I' && in.read() == 'F'); } catch(Exception e){e.printStackTrace();} finally { try { in.close(); } catch (IOException ignore) {} } return false; } //private static final URI ABSOLUTE_URI = URI.create("Absolute"); private boolean isRelative() { return mRelativeURI != null; } private void setRelativeURI(URI relative) { mRelativeURI = relative; if (relative != null) { setProperty(FILE_RELATIVE, relative); setProperty(USER_FILE, getRelativePath()); setProperty(USER_FULL_FILE, mFile); } else { removeProperty(FILE_RELATIVE); removeProperty(USER_FULL_FILE); // what if there's still a canonical difference? setProperty(USER_FILE, mFile); } } private String getRelativePath() { return mRelativeURI == null ? null : mRelativeURI.getPath(); } // @Override // public void updateIfRelativeTo(URI root) // { // if (!isRelative()) // setRelativeURI(findRelativeURI(root)); // } @Override public void recordRelativeTo(URI root) { setRelativeURI(findRelativeURI(root)); // final URI relative = findRelativeURI(root); // if (relative != null) { // //Log.debug("made-relative: " + relative + "; " + this); // setProperty(FILE_RELATIVE, relative); // } else { // //Log.debug(" no-relative: " + this); // removeProperty(FILE_RELATIVE); // } // if (true||isLocalFile()) { // URI relative = findRelativeURI(root); // if (relative != null) { // Log.debug("made-relative: " + relative + "; " + this); // setProperty(FILE_RELATIVE, relative); // } else { // Log.debug(" no-relative: " + this); // removeProperty(FILE_RELATIVE); // } // } else { // Log.debug(" non-relative: " + this); // removeProperty(FILE_RELATIVE); // } } /** @return a unique URI for this resource */ private java.net.URI toAbsoluteURI() { if (mFile != null) return toCanonicalFile(mFile).toURI(); else if (mURL != null) return makeURI(mURL); else return makeURI(getSpec()); } private URI findRelativeURI(URI root) { final URI absURI = toAbsoluteURI(); if (root.getScheme() == null || !root.getScheme().equals(absURI.getScheme())) { //if (DEBUG.Enabled) out("differing schemes: " + root + " - " + absURI + "; can't be relative"); if (DEBUG.RESOURCE) Log.info(this + "; scheme=" + absURI.getScheme() + "; different scheme: " + root + "; can't be relative"); return null; } // if (DEBUG.Enabled) { // //System.out.println("\n======================================================="); // Log.debug("attempting to relativize [" + this + "] against: " + root); // } if (!absURI.isAbsolute()) Log.warn("findRelativeURI: non-absolute URI: " + absURI); //Log.warn("Non absolute URI: " + absURI + "; from URL " + url); // if (absURI == null) { // System.out.println("URL INVALID FOR URI: " + url + "; in " + this); // return null; // } if (DEBUG.RESOURCE) Resource.dumpURI(absURI, "CURRENT ABSOLUTE:"); final URI relativeURI = root.relativize(absURI); if (relativeURI == absURI) { // oldRoot was unable to relativize absURI -- this resource // was not relative to it's map in it's previous incarnation. return null; } if (relativeURI != absURI) { if (DEBUG.RESOURCE) Resource.dumpURI(relativeURI, "RELATIVE FOUND:"); } if (DEBUG.Enabled) { out(TERM_GREEN+"FOUND RELATIVE: " + relativeURI + TERM_CLEAR); } else { Log.info("found relative to " + root + ": " + relativeURI.getPath()); } return relativeURI; } /** @return a URI from a string that was known to already be properly encoded as a URI */ private URI rebuildURI(String s) { return URI.create(s); } @Override public void restoreRelativeTo(URI root) { // Even if the existing original resource exists, we always // choose the relative / "local" version, if it can be found. String relative = getProperty(FILE_RELATIVE_OLD); if (relative == null) { relative = getProperty(FILE_RELATIVE); if (relative == null) { // attempt to find us in case we're relative anyway: //recordRelativeTo(root); return; // nothing to do } } else { removeProperty(FILE_RELATIVE_OLD); setProperty(FILE_RELATIVE, relative); } final URI relativeURI = rebuildURI(relative); final URI absoluteURI = root.resolve(relativeURI); if (DEBUG.RESOURCE) { System.out.print(TERM_PURPLE); Resource.dumpURI(absoluteURI, "resolved absolute:"); Resource.dumpURI(relativeURI, "from relative:"); System.out.print(TERM_CLEAR); } if (absoluteURI != null) { final File file = new File(absoluteURI); if (file.canRead()) { // only change the spec if we can actually find the file (todo: test Vista -- does canRead work?) if (DEBUG.RESOURCE) setDebugProperty("relative URI", relativeURI); Log.info(TERM_PURPLE + "resolved " + relativeURI.getPath() + " to: " + file + TERM_CLEAR); setRelativeURI(relativeURI); setSpecByFile(file); } else { out_warn(TERM_RED + "can't find data relative to " + root + " at " + relative + "; can't read " + file + TERM_CLEAR); // todo: should probably delete the relative property key/value at this point } } else { out_error("failed to find relative " + relative + "; in " + root + " for " + this); } } // public static final boolean ALLOW_URI_WHITESPACE = false; // Not working yet // /** @deprecated */ // @Override // public void makeRelativeTo(URI root) // { // if (true) { // // TODO: this code is for backward compat with archive version #1. // // We may be able to remove it in short order. This is called // // by the map after restoring. Only archive version #1 resources // // should ever have a PACKAGE_KEY property set tho, so it's // // safe to leave this code in. // // When dealing with a packaged resource, Resources that were originally // // local-file will want to be re-written to point to the actual new local // // package cache file. But resources that we're NOT local will want to have // // their resource spec's left alone, yet have their content actually pulled from // // the local cache. We can determine later if we want live updating from the // // original web source of the data, or provide a user action for that. // if (hasProperty(PACKAGE_KEY_DEPRECATED)) { // String packageLocal = getProperty(PACKAGE_KEY_DEPRECATED); // Log.info("Found old-style package key on " + this + "; " + packageLocal); // if (ALLOW_URI_WHITESPACE) { // // URI.create fails if there are spaces: // packageLocal = packageLocal.replaceAll(" ", "%20"); // } // URI packaged = root.resolve(packageLocal); // if (packaged != null) { // Log.debug("Found packaged: " + packaged); // //this.spec = SPEC_UNSET; // //mRelativeURI = null; // // WE NO LONGER SET SPEC FOR WEB CONTENT: fetch PACKAGED_KEY when getting data (need new API for that..) // if (isLocalFile()) { // // If the original was a local file (e.g., on some other user's machine), // // completely reset the spec, as it will have no meaning on the new // // users machine. // setSpec(packaged.toString()); // } // if ("file".equals(packaged.getScheme())) { // setProperty(PACKAGE_FILE, packaged.getRawPath()); // be sure to use getRawPath, otherwise will decode octets // setCached(true); // will let thumbnail requests go to cache file instead // } else { // Log.warn("Non-file URI-scheme in resolved packaged URI: " + packaged); // setProperty(PACKAGE_FILE, packaged.toString()); // } // return; // } // } // } // if (!isLocalFile()) { // Log.debug("Remote, unpackaged file, skipping relativize: " + this); // return; // } // // if (true) { // // // incomplete // // if (DEBUG.Enabled) Log.debug("Relativize to " + root + "; " + this + "; curRelative=" + mRelativeURI); // // URI oldRelative = mRelativeURI; // // mRelativeURI = findRelativeURI(root); // // setDebugProperty("relative", mRelativeURI); // // if (oldRelative != mRelativeURI && !oldRelative.equals(mRelativeURI)) { // // invalidateToolTip(); // // } // // } // } // /** // * If this resource can be made relative to the current map (is in a directory // * below the current map), make sure we record it's relative location. // * If oldRoot and newRoot are different (the map has moved), re-write // * the resource to point to the new location if something is there. // * // * @param oldRoot - the root (parent directory) of the map the last time it was saved // * @param newRoot - null if the same as oldRoot, otherwise, the newRoot // */ // // ONLY USED FOR OLD STYLE AUTO-CONVERSION ON STARTUP // @Override // public void updateRootLocation(URI oldRoot, URI newRoot) { // if (DEBUG.Enabled) { // System.out.println(); // Log.debug("attempting to relativize [" + this + "] against curRoot " + oldRoot + "; newRoot " + newRoot); // } // final URL url = asURL(); // if (url == null) // return; // System.out.println("======================================================="); // final URI absURI = makeURI(url.toString()); // // absURI should always be absolute -- the way we persist them // if (!absURI.isAbsolute()) // Log.warn("Non absolute URI: " + absURI + "; from URL " + url); // if (absURI == null) { // System.out.println("URL INVALID FOR URI: " + url + "; in " + this); // return; // } // Resource.dumpURI(absURI, "ORIGINAL"); // final URI relativeURI = oldRoot.relativize(absURI); // if (relativeURI == absURI) { // // oldRoot was unable to relativize absURI -- this resource // // was not relative to it's map in it's previous incarnation. // // However, if newRoot is different from oldRoot, // // it may be relative to the new map location (newRoot). // if (newRoot == null) // was same as oldRoot // return; // } // if (relativeURI != absURI) // Resource.dumpURI(relativeURI, "RELATIVE"); // //System.out.println(" RELATIVE URI: " + relativeURI); // //System.out.println("RELATIVE PATH: " + relativeURI.getPath()); // if (newRoot != null) { // if (relativeURI.isAbsolute()) { // meaning relativeURI == absURI // //------------------------------------------------------- // // was absolute: attempt to relativize against newRoot // //------------------------------------------------------- // if (relativeURI != absURI) Log.warn("URLResource assertion failure: " + relativeURI + "; " + absURI); // Log.debug("ATTEMPTING TO RELATIVIZE AGAINST NEW ROOT: " + relativeURI + "; " + newRoot); // final URI newRelativeURI = newRoot.relativize(relativeURI); // if (newRelativeURI != relativeURI) { // System.out.println(TERM_GREEN+"NOTICED NEW RELATIVE: " + newRelativeURI + TERM_CLEAR); // mRelativeURI = newRelativeURI; // } // } else { // //------------------------------------------------------- // // was relative: attempt to resolve against newRoot // //------------------------------------------------------- // Log.debug("ATTEMPTING RESOLVE AGAINST NEW ROOT: " + relativeURI + "; " + newRoot); // final URI newAbsoluteURI = newRoot.resolve(relativeURI); // final File newFile = new File(newAbsoluteURI.getPath()); // if (newFile.exists()) { // System.out.println(TERM_GREEN+" FOUND NEW LOCATION: " + newFile + TERM_CLEAR); // spec = newAbsoluteURI.getRawPath(); // // File was found at same relative location: // mRelativeURI = relativeURI; // mURL_Default = null; // reset // } else { // // File was NOT found same relative location -- // // leave this Resource as it's old absolute value. // mRelativeURI = null; // } // } // } else if (relativeURI != absURI) { // mRelativeURI = relativeURI; // System.out.println(TERM_GREEN+" FOUND NEW RELATIVE: " + relativeURI + TERM_CLEAR); // } // invalidateToolTip(); // } /** * This impl will return true the FIRST time after the data has changed, * and subsequent calls will return false, until the data changes again. * This currently only monitors local disk resources (e.g., not web resources). */ @Override public boolean dataHasChanged() { final File file; if (mDataFile != null) file = mDataFile; // in case user edits a package file else file = mFile; if (file != null) { // Not an ideal impl, as only the first caller will find out if the data has // changed. Ideally, Resources will have to be enforced atomic (at least // for local file resources), and track all listeners/owners, so when/if an // udpate happens, they can all be notified. Or, the called can just take // care of finding all objects that need updating once this ever returns // true. // TODO: this is mainly for updating images -- this would // be better handled in the Images cache / ImageRef. if (DEBUG.Enabled||DEBUG.IO) dumpField("re-scanning", file); final long curLastMod = file.lastModified(); final long curSize = file.length(); if (curLastMod != mLastModified || curSize != getByteSize()) { if (DEBUG.Enabled) { long diff = curLastMod - mLastModified; out_info(TERM_CYAN + Util.tags(file) + "; lastMod=" + new Date(curLastMod) + "; timeDelta=" + (diff/100) + " seconds" + "; sizeDelta=" + (curSize-getByteSize()) + " bytes" + TERM_CLEAR); } //if (DEBUG.Enabled) out("lastModified: dataHasChanged"); mLastModified = curLastMod; if (curSize != getByteSize()) setByteSize(curSize); return true; } } return false; } @Override public String getLocationName() { File archive = null; try { archive = (File) getPropertyValue(PACKAGE_ARCHIVE); } catch (Throwable t) { Log.warn(this, t); } if (archive == null) { if (isRelative()) return getRelativePath(); else return getSpec(); } else if (hasProperty(USER_URL)) { return getProperty(USER_URL); } else { final String name; if (mDataFile != null) name = mDataFile.getName(); else if (mTitle != null) name = mTitle; else name = getSpec(); return String.format("%s(%s)", archive.getName(), name); } } /** @see tufts.vue.Resource */ @Override public Object getImageSource() { // Object is = _getImageSource(); // //Log.debug(this + "; getImageSource returns " + Util.tags(is)); // return is; // } // private Object _getImageSource() { // What would happen if we allowed returning the Images cache file here? // Image code is presumably already checking for that... if (mDataFile != null) { return mDataFile; } else if (mURL_ImageData != null) { return mURL_ImageData; } else { if (mURL == null && getClientType() != NONE) { // This can happen if we're point to a missing local file. // Also, if clientType is NONE, this is normal: e.g., a C:\file\path resource // opened on a Mac. if (DEBUG.RESOURCE) { Log.warn("mURL == null, likely missing file.", new Throwable(toString())); } else { Log.warn("mURL == null, likely missing file: " + this); } } return mURL; } } @Override public int hashCode() { return spec == null ? super.hashCode() : spec.hashCode(); } @Override public boolean equals(Object o) { if (o == this) return true; if (o instanceof Resource) { if (spec == SPEC_UNSET || spec == null) return false; final String spec2 = ((Resource)o).getSpec(); if (spec2 == SPEC_UNSET || spec2 == null) return false; return spec.equals(spec2); } return false; } // /** @return a URL if possible to provide a valid one on this platform, or null if unable to create one */ // @Override // public java.net.URL asURL() // { // // if (mURL == null) { // // if (spec != SPEC_UNSET) // // setURL(makeURL(this.spec)); // // } // // out("asURL returns " + Util.tags(mURL)); // return mURL; // } private Object getBrowseReference() { if (mURL != null) return mURL; else if (mFile != null) return mFile; else if (mDataFile != null) return mDataFile; else return getSpec(); } public void displayContent() { final Object contentRef = getBrowseReference(); out("displayContent: " + Util.tags(contentRef)); final String systemSpec = contentRef.toString(); try { markAccessAttempt(); VueUtil.openURL(systemSpec); // access successful is not currently very meaningful, // as we don't know if the openURL failed or not. markAccessSuccess(); } catch (Throwable t) { Log.error(systemSpec + "; " + t); } tufts.vue.gui.VueFrame.setLastOpenedResource(this); } // public void displayContent() { // final String systemSpec; // if (hasProperty(PACKAGE_FILE)) { // systemSpec = getPackagedURL().toString(); // } // else if (mURL != null) { // systemSpec = mURL.toString(); // TODO TODO TODO: here's the problem. mURL is what's a mess REFACTOR AGAIN... // } // // else if (VueUtil.isMacPlatform()) { // // // toURL will fail if we have a Windows style "C:\Programs" url, so // // // just in case don't try and construct a URL here. // // systemSpec = toURLString(); // // } // else // systemSpec = getSpec(); // try { // markAccessAttempt(); // VueUtil.openURL(systemSpec); // // access successful is not currently very meaningful, // // as we don't know if the openURL failed or not. // markAccessSuccess(); // } catch (Exception e) { // //System.err.println(e); // Log.error(systemSpec + "; " + e); // } // } public void setTitle(String title) { if (mTitle == title) return; // title = mURL.getPath(); // if (title != null) { // if (title.endsWith("/")) // title = title.substring(0, title.length() - 1); // title = title.substring(title.lastIndexOf('/') + 1); // if (tufts.Util.isMacPlatform()) { // // On MacOSX, file names with colon (':') in them display as slashes ('/') // title = title.replace(':', '/'); // } // setTitle(title); // } //if (DEBUG.DATA || (DEBUG.RESOURCE && DEBUG.META)) dumpField("setTitle", title); mTitle = org.apache.commons.lang.StringEscapeUtils.unescapeHtml(title); if (DEBUG.RESOURCE) { dumpField("setTitle", title); //if ("A:\\".equals(title)) Util.printStackTrace(this.toString()); if (hasProperty(DEBUG_PREFIX + "title")) setDebugProperty("title", title); } } public String getTitle() { return mTitle; } // private String X_toURLString() { // String s = this.spec.trim(); // final char c0 = s.length() > 0 ? s.charAt(0) : 0; // final char c1 = s.length() > 1 ? s.charAt(1) : 0; // final String txt; // // In case there are any special characters (e.g., Unicode chars) in the // // file name, we must first encode them for MacOSX (local files only?) // // FYI, MacOSX openURL uses UTF-8, NOT the native MacRoman encoding. // // URLEncoder encodes EVERYTHING other than alphas tho, so we need // // to put it back. // // But first we DECODE it, in case there are already any encodings, // // we don't want to double-encode. // //TODO: url = java.net.URLDecoder.decode(url, "UTF-8"); // //TODO: if (DEBUG) System.err.println(" DECODE UTF [" + url + "]"); // // TODO ALSO: cache file not being %20 normalized (Seeing %252520 !) // if (c0 == '/' || c0 == '\\' || (Character.isLetter(c0) && c1 == ':')) { // // first case: MacOSX path // // second case: Windows path // // third case: Windows "C:" style path // // TODO: consider using URN's for this, which have some code // // for this type of resolution. // txt = "file://" + s; // Util.printStackTrace("toURLString FILE:// -ified: " + txt); // //Log.debug("toURLString produced " + txt); // } else { // txt = s; // } // //if (DEBUG.Enabled) out("toURLString[" + txt + "]"); // /* // // from old LWImage code: // if (s.startsWith("file://")) { // // TODO: SEE Util.java: WINDOWS URL'S DON'T WORK IF START WITH FILE:// // // (two slashes), MUST HAVE THREE! move this code to MapResource; find // // out if can even force a URL to have an extra slash in it! Report // // this as a java bug. // // TODO: Our Cup>>Chevron unicode char example is failing // // here on Windows (tho it works for windows openURL). // // (The image load fails) // // Try ensuring the URL is UTF-8 first. // s = s.substring(7); // if (DEBUG.IMAGE || DEBUG.THREAD) out("getImage " + s); // image = java.awt.Toolkit.getDefaultToolkit().getImage(s); // } else { // */ // /* // if (this.url == null) { // // [old:this logic shouldn't be needed: if spec can be a a valid URL, this.url will already be set] // if (spec.startsWith("file:") || spec.startsWith("http:") || spec.startsWith("ftp:")) { // //System.err.println(getClass() + " BOGUS URLResource: is URL, but unrecognized! " + this); // txt = spec; // } // // todo: handle "resource:" case // else // txt = "file:///" + spec; // if (DEBUG.Enabled) out("toURLString[" + txt + "]"); // } else // txt = this.url.toString(); // */ // //if (!spec.startsWith("file") && !spec.startsWith("http")) // // txt = "file:///" + spec; // return txt; // } /* private java.net.URL toURL_OLD() throws java.net.MalformedURLException { if (spec.equals(SPEC_UNSET)) return mURL; if (mURL == null) { if (spec.startsWith("resource:")) { final String classpathResource = spec.substring(9); //System.out.println("Searching for classpath resource [" + classpathResource + "]"); mURL = getClass().getResource(classpathResource); } else mURL = new java.net.URL(toURLString()); mURL = new java.net.URL(toURLString()); setProperty("Content.type", java.net.URLConnection.guessContentTypeFromName(mURL.getPath())); // This no longer makes sense as we're now more like an Asset, and // no single URL part need be singled out. mProperties.holdChanges(); try { // todo: do this once on constrution of a URLResource setProperty("URL.protocol", mURL.getProtocol()); setProperty("URL.userInfo", mURL.getUserInfo()); setProperty("URL.host", mURL.getHost()); setProperty("URL.authority", mURL.getAuthority()); setProperty("URL.path", mURL.getPath()); // setProperty("url.file", url.getFile()); // same as path (doesn't get us stub after last /) setProperty("URL.query", mURL.getQuery()); setProperty("URL.ref", mURL.getRef()); //setProperty("url.authority", url.getAuthority()); // always same as host? if (mURL.getPort() != -1) setProperty("URL.port", mURL.getPort()); //setProperty(CONTENT_TYPE, setProperty("Content.type", java.net.URLConnection.guessContentTypeFromName(mURL.getPath())); } finally { mProperties.releaseChanges(); } if ("file".equals(mURL.getProtocol())) { this.type = Resource.FILE; if (mTitle == null) { String title; title = mURL.getPath(); if (title != null) { if (title.endsWith("/")) title = title.substring(0, title.length() - 1); title = title.substring(title.lastIndexOf('/') + 1); if (tufts.Util.isMacPlatform()) { // On MacOSX, file names with colon (':') in them display as slashes ('/') title = title.replace(':', '/'); } setTitle(title); } } } else { this.type = Resource.URL; } } return mURL; } */ /** Return exactly whatever we were handed at creation time. We * need this because if it's a local file (file: URL or just local * file path name), we need whatever the local OS gave us as a * reference in order to give that to give back to openURL, as * it's the most reliable string to give back to the underlying OS * for opening a local file. */ public String getSpec() { //if (DEBUG.RESOURCE && DEBUG.META) dumpField("getSpec", spec); return this.spec; } // This currently redundant with the property for this we're using, but it's // in the mapping and some old save files might have it set, so we're keeping // set/get RelativeURI around. public String getRelativeURI() { return null; // if (mRelativeURI != null) // return mRelativeURI.toString(); // else // return null; } /** persistance only */ public void setRelativeURI(String s) { // if (!mRestoreUnderway) { // Util.printStackTrace("only allowed for persistance; setRelativeURI " + s); // return; // } // mRelativeURI = makeURI(s); } /* * If isLocalFile is true, this will return a file name * suitable to be given to java.io.File such that it * can be found. Note that this may differ from getSpec. * If isLocalFile is false, it will return the file * portion of the URL, although that may not be useful. public String getFileName() { if (mURL == null) return getSpec(); else return url.getFile(); } */ /** this is only meaninful if this resource points to a local file */ protected Image getFileIconImage() { return GUI.getSystemIconForExtension(getDataType(), 128); } @Override public boolean isLocalFile() { return mFile != null || (mURL != null && "file".equals(mURL.getProtocol())); // todo: eventually shouldn't need 2nd check // if (false) { // //if (hasProperty(PACKAGE_FILE)) { // // todo: make sure this isn't overkill... // return true; // } else { // asURL(); // return mURL == null || mURL.getProtocol().equals("file"); // //String s = spec.toLowerCase(); // //return s.startsWith("file:") || s.indexOf(':') < 0; // } } // public String getExtension() { // final String r = getSpec(); // String ext = "xxx"; // if (r.startsWith("http")) // ext = "web"; // else if (r.startsWith("file")) // ext = "file"; // else { // ext = r.substring(0, Math.min(r.length(), 3)); // if (!r.endsWith("/")) { // int i = r.lastIndexOf('.'); // if (i > 0 && i < r.length()-1) // ext = r.substring(i+1); // } // } // if (ext.length() > 4) // ext = ext.substring(0,4); // return ext; // } // /** // * getPropertyNames // * This returns an array of property names // * @return String [] the list of property names // **/ // public String [] getPropertyNames() { // if( (mPropertyNames == null) && (!mProperties.isEmpty()) ) { // Set keys = mProperties.keySet(); // if( ! keys.isEmpty() ) { // mPropertyNames = new String[ keys.size() ]; // Iterator it = keys.iterator(); // int i=0; // while( it.hasNext()) { // mPropertyNames[i] = (String) it.next(); // i++; // } // } // } // return mPropertyNames; // } /** @deprecated */ public void setProperties(Properties p) { tufts.Util.printStackTrace("URLResource.setProperties: deprecated " + p); } /* public Map getPropertyMap() { System.out.println(this + " *** getPropertyMap " + mProperties); return mProperties; } public void setPropertyMap(Map m) { System.out.println(this + " *** setPropertyMap " + m.getClass().getName() + " " + m); mProperties = (Properties) m; } */ /** this is for castor persistance only */ public java.util.List getPropertyList() { if (mRestoreUnderway == false) { if (mProperties.size() == 0) // a hack for castor to work return null; mXMLpropertyList = new ArrayList(mProperties.size()); //for (Map.Entry<String,?> e : mProperties.entries()) for (Map.Entry e : mProperties.entries()) mXMLpropertyList.add(new PropertyEntry(e)); // E.g., if using a multi-map: (or provide an asMap() view) // for (Map.Entry<String,?> e : mProperties.flattendEntries()) // Iterator i = mProperties.keySet().iterator(); // while (i.hasNext()) { // final String key = (String) i.next(); // final PropertyEntry entry = new PropertyEntry(); // entry.setEntryKey(key); // entry.setEntryValue(mProperties.get(key)); // mXMLpropertyList.add(entry); // } } if (DEBUG.CASTOR) System.out.println(this + " getPropertyList " + mXMLpropertyList); return mXMLpropertyList; } public void XML_initialized(Object context) { if (DEBUG.CASTOR) System.out.println(getClass() + " XML INIT"); mRestoreUnderway = true; mXMLpropertyList = new ArrayList(); } public void XML_fieldAdded(Object context, String name, Object child) { if (DEBUG.XML) out("XML_fieldAdded <" + name + "> = " + child); } public void XML_addNotify(Object context, String name, Object parent) { if (DEBUG.CASTOR) System.out.println(this + " XML ADDNOTIFY as \"" + name + "\" to parent " + parent); } private static boolean isImageMimeType(final String s) { return s != null && s.toLowerCase().startsWith("image/"); } private static boolean isHtmlMimeType(final String s) { return s != null && s.toLowerCase().startsWith("text/html"); } private static final String UNSET = "<unset-mimeType>"; private String mimeType = UNSET; @Override protected String determineDataType() { // TODO: clean this up / cache more of the result / can we eliminate this fedora hack // yet (it DRAMATICALLY slows down obtaining fedora search results) final String spec = getSpec(); if (spec.endsWith("=jpeg")) { // special case for MFA data source -- TODO: MFA OSID should handle this return "jpeg"; } else if (mimeType != UNSET) { return mimeType; } else if (spec != SPEC_UNSET && spec.startsWith("http") && spec.contains("fedora")) { // fix for fedora url // special case for Fedora data source -- TODO: FEDORA OSID should handle this if (spec.endsWith("bdef:AssetDef/getFullView/")) { // this dramatically speeds up obtaining fedora search results //setProperty(CONTENT_TYPE, "text/html"); // tho don't set this unless actually verified return "html"; } else { // if previously determined (e.g., was persisted), don't bother to look-up again String type = getProperty(CONTENT_TYPE); if (type == null || type.length() < 1) { try { final URL url = (mURL != null ? mURL : new URL(getSpec())); // TODO: checking spec, which defaults to the "browse" URL, will not get // the real content-type in cases (such as fedora!) where the browse // url is always an HTML page that includes the image with some descrition text. //Log.info("opening URL " + url); if (DEBUG.Enabled) out("polling actual HTTP server for content-type: " + url); /** * If you try to get Headerfield on some websites, notably * www.fedoracommons.org from an applet in Firefox on the mac it will hang * Java and firefox and you have to force quit the application. */ if (!VUE.isApplet()) type = url.openConnection().getHeaderField("Content-type"); else type = null; if (DEBUG.Enabled) { out("got contentType " + url + " [" + type + "]"); //Util.printStackTrace("GOT CONTENT TYPE"); } if (type != null && type.length() > 0) setProperty(CONTENT_TYPE, type); } catch (Throwable t) { Log.error("content-type-fetch: " + this, t); } } if (type != null && type.contains("/")) { mimeType = type.split("/")[1]; // returning the second part of mime-type if (mimeType.indexOf(';') > 0) { // remove charset - e.g.: "text/html;charset=UTF-8" mimeType = mimeType.substring(0, mimeType.indexOf(';')); } //return "html".equals(mimeType) ? EXTENSION_HTTP : mimeType; return mimeType; } } } return super.determineDataType(); } private static boolean isHTML(final Resource r) { String s = r.getSpec().toLowerCase(); if (s.endsWith(".html") || s.endsWith(".htm")) return true; // todo: why .vue files reporting as text/html on MacOSX to content scraper? return !s.endsWith(".vue") && isHtmlMimeType(r.getProperty("url.contentType")) //&& !isImage(r) // sometimes image files claim to be text/html ; } public boolean isHTML() { if (isImage()) return false; else return isHTML(this); } //private boolean isHTML() { return !isImage(); } // TODO: combine these into a constructor only for integrity (e.g., Osid2AssetResource is factory only) /** Currently, this just calls setSpec -- the "browse" URL is the default URL */ protected void setURL_Browse(String s) { if (DEBUG.RESOURCE) dumpField("setURL_Browse", s); setSpec(s); } public void setURL_Thumb(String s) { if (DEBUG.RESOURCE) dumpField("setURL_Thumb", s); // TODO performance: don't need to do this until thumbnail is requested mURL_ThumbData = makeURL(s); setProperty(THUMB_KEY, mURL_ThumbData); } /** If given any valid URL, this resource will consider itself image content, no matter * what's at the other end of that URL, so care should be taken to ensure it's * valid image data (as opposed to say, an HTML page) */ protected void setURL_Image(String s) { if (DEBUG.RESOURCE) dumpField("setURL_Image", s); mURL_ImageData = makeURL(s); setProperty(IMAGE_KEY, mURL_ImageData); if (mURL_ImageData != null) setAsImage(true); } /** * Either immediately return an Image object if available, otherwise return an * object that is some kind of valid image source (e.g., a URL or image Resource) * that can be fed to Images.getImage and fetch asynchronously w/callbacks if it * isn't already in the cache. */ public Object getPreview() { if (isCached() && isImage()) return this; else if (mURL_ThumbData != null) return mURL_ThumbData; else if (mURL_ImageData != null) return mURL_ImageData; else if (isImage()) // returning "this" is a bit unclean: done so that Images.java can put meta-data back into us return this; else if (isLocalFile() || getClientType() == Resource.FILE || getClientType() == Resource.DIRECTORY) { return getFileIconImage(); } else if (mURL != null && !isLocalFile()) //else if (getClientType() == Resource.URL && !isLocalFile()) { //System.out.println("mURL : " + mURL); if (mURL.toString().toLowerCase().endsWith(VueUtil.VueExtension)) return VueResources.getBufferedImage("vueIcon64x64"); else return getThumbshotURL(mURL); // if (mThumbShot == null) { // mThumbShot = fetchThumbshot(mURL); // // If we don't assign this, it will keep trying, which // // is bad, yet if we go from offline to online, we'd // // like to start finding these, so we just keep trying for now... // //if (mThumbShot == null) mThumbShot = GUI.NoImage32; // } // return mThumbShot; } else return null; /* if (mPreview == null) { // TODO: this currently special case to Osid2AssetResource, tho names are somewhat generic.. URL url = null; //url = makeURL(getProperty("mediumImage")); url = makeURL(getProperty("smallImage")); if (url == null) url = makeURL(getProperty("thumbnail")); if (url == null) { // TODO: Hack for MFA until Resource has API for setting Asset-like meta-data url = makeURL(getProperty("Preview Or Thumbnail")); if (DEBUG.RESOURCE) tufts.Util.printStackTrace("got MFA preview " + url); } if (url != null) mPreview = url; } return mPreview; */ } public static final String THUMBSHOT_FETCH = "http://open.thumbshots.org/image.pxf?url="; private URL getThumbshotURL(URL url) { if (true) // I don't think thumbshots ever generate images for paths beyond the root host: return makeURL(String.format("%s%s://%s/", THUMBSHOT_FETCH, url.getProtocol(), url.getHost())); else return makeURL(THUMBSHOT_FETCH + url); } // TODO: May be able to replace deprecated Mac Cocoa<->Java code for icon fetches by // using a JFileChooser (which FYI, may possibly get better results if AWT UI peer is // created), tho the objects we get back are icons, not images, which will limit us some // -- would probably need to combine code like the below which changes to // GUI.getSystemIconForExtension to properly handle this. -- SMF March 2009 // See Mac Java tip of Dec 2008: http://nadeausoftware.com/node/89 // private static final javax.swing.JFileChooser FileView = new javax.swing.JFileChooser(); // @Override // protected ImageIcon makeIcon(int size, int max) // { // if (mFile != null) { // Icon icon = FileView.getIcon(mFile); // Log.debug("got " + Util.tags(icon) + " from " + mFile); // if (icon instanceof ImageIcon) { // Log.debug("is ImageIcon"); // return (ImageIcon) icon; // } // } // return super.makeIcon(size, max); // } /** @deprecated -- for backward compat with lw_mapping_1.0.xml only, where this never worked */ public void setPropertyList(Vector propertyList) { // This actually never get's called, but the old mapping file demands that it's here. Log.info("IGNORING OLD SAVE FILE DATA " + propertyList + " for " + this); } private static String deco(String s) { return "<i><b>"+s+"</b></i>"; } // private volatile String mToolTipHTML; // @Override // public String getToolTipText() { // if (mToolTipHTML == null || DEBUG.META) // mToolTipHTML = buildToolTipHTML(); // return mToolTipHTML; // } private void invalidateToolTip() { //mToolTipHTML = null; } // private String buildToolTipHTML() { // String pretty = ""; // // if (mURI != null) { // // if (mURI.isAbsolute()) { // // pretty = deco(mURI.toString()); // // if (DEBUG.Enabled) pretty += " (URI-FULL)"; // // } else { // // pretty = deco(mURI.getPath()); // // if (DEBUG.Enabled) pretty += " (URIpath)"; // // } // // } else { // pretty = VueUtil.decodeURL(getSpec()); // if (pretty.startsWith("file://") && pretty.length() > 7) // pretty = pretty.substring(7); // if (DEBUG.Enabled) { // if (pretty.equals(getSpec())) // pretty += " (spec)"; // else // pretty += " (decoded spec)"; // } // pretty = deco(pretty); // //} // if (DEBUG.Enabled) { // //final String nl = "<br> "; // final String nl = "<br>"; // pretty += nl + spec + " (spec)"; // //if (mRelativeURI != null) pretty += nl + "URI-RELATIVE: " + mRelativeURI; // pretty += nl + String.format("%s ext=[%s]", asDebug(), getDataType()); // // pretty += nl + "type=" + TYPE_NAMES[getClientType()] + "(" + getClientType() + ")" // // + " impl=" + getClass().getName() + " ext=[" + getContentType() + "]"; // if (isLocalFile()) // pretty += " (isLocal)"; // //pretty += nl + "localFile=" + isLocalFile(); // } // return pretty; // } /** * Search for meta-data: e.g., * * HTTP meta-data (contentType, size) * HTML meta-data (title) * FileSystem meta-data (e.g., Spotlight) * * Will set properties in the resource based on what's found, * and may update the title. * */ /* * E.g. scan an initial chunk of our content for an HTML title * tag, and if one is found, set our title field to what we find * there. RUNS IN IT'S OWN THREAD. If the give LWC's label is * the same as the title at the start, that is updated also. * * TODO: resources need listeners so they can issue model * changes/signals, and we need to run this in an undoable thread. * * TODO: this may set the component label! Either do that * in the caller instead of here, or rename this. Altho, * one of the reasons it does that is that as this happens * async, it has to do that, as we can't return a value running async... */ public void scanForMetaDataAsync(final LWComponent c) { scanForMetaDataAsync(c, false); } public void scanForMetaDataAsync(final LWComponent c, final boolean setLabelFromTitle) { //if (!isHTML() && !isImage()) { // [oops, "http://www.google.com/" doesn't initially appear as HTML] // don't bother with these for now: would only get us size & content-type // anyway, which if it's a file should come from the CabinetResource //return; //} new Thread("VUE-URL-MetaData") { public void run() { scanForMetaData(c, setLabelFromTitle); } }.start(); } void scanForMetaData(final LWComponent c, boolean setLabelFromTitle) { if (true) { //System.out.println("SKIPPING META-DATA SCAN " + this); return; } //URL _url = asURL(); URL _url = mURL; if (_url == null) { if (DEBUG.Enabled) out("couldn't get URL"); return; } final boolean forceTitleToLabel = setLabelFromTitle || c.getLabel() == null || c.getLabel().equals(mTitle) || c.getLabel().equals(getSpec()); try { _scanForMetaData(_url); } catch (Throwable t) { Log.info(_url + ": meta-data extraction failed: " + t); if (DEBUG.Enabled) tufts.Util.printStackTrace(t, _url.toString()); } if (forceTitleToLabel && getTitle() != null) c.setLabel(getTitle()); if (DEBUG.Enabled) out("properties " + mProperties); } private void _scanForMetaData(URL _url) throws java.io.IOException { if (DEBUG.Enabled) System.out.println(this + " _scanForMetaData: xml props " + mXMLpropertyList); // TODO: split into scrapeHTTPMetaData for content type & size, // and scrapeHTML meta-data for title. Tho really, we need // at this point to start having a whole pluggable set of content // meta-data scrapers. if (DEBUG.Enabled) System.out.println("*** Opening connection to " + _url); markAccessAttempt(); Properties metaData = scrapeHTMLmetaData(_url.openConnection(), 2048); if (DEBUG.Enabled) System.out.println("*** Got meta-data " + metaData); markAccessSuccess(); String title = metaData.getProperty("title"); if (title != null && title.length() > 0) { setProperty("title", title); title = title.replace('\n', ' ').trim(); setTitle(title); } try { setByteSize(Integer.parseInt((String) getProperty("contentLength"))); } catch (Exception e) {} } // TODO: need to handle <title lang=he> example (is that legal HTML?) -- //private static final Pattern HTML_Title = Pattern.compile(".*<\\s*title\\s*>\\s*([^<]+)", // did we need .* at end? // need to ensure there is a space after title or the '>' immediately: don't want to match a tag that was <title-i-am-not> ! private static final Pattern HTML_Title_Regex = Pattern.compile(".*<\\s*title[^>]*>\\s*([^<]+)", // hacked for lang=he constructs, but too broad Pattern.MULTILINE|Pattern.DOTALL|Pattern.CASE_INSENSITIVE); private static final Pattern Content_Charset_Regex = Pattern.compile(".*charset\\s*=\\s*([^\">\\s]+)", Pattern.MULTILINE|Pattern.DOTALL|Pattern.CASE_INSENSITIVE); // TODO: break out searching into looking for regex with each chunk of data we get in at least x size (e.g, 256) private Properties scrapeHTMLmetaData(URLConnection connection, int maxSearchBytes) throws java.io.IOException { Properties metaData = new Properties(); InputStream byteStream = connection.getInputStream(); if (DEBUG.DND && DEBUG.META) { System.err.println("Getting headers from " + connection); System.err.println("Headers: " + connection.getHeaderFields()); } // note: be sure to call getContentType and don't rely on getting it from the HeaderFields map, // as sometimes it's set by the OS for a file:/// URL when there are no header fields (no http server) // (actually, this is set by java via a mime type table based on file extension, or a guess based on the stream) if (DEBUG.DND) System.err.println("*** getting contentType & encoding..."); final String contentType = connection.getContentType(); final String contentEncoding = connection.getContentEncoding(); final int contentLength = connection.getContentLength(); if (DEBUG.DND) System.err.println("*** contentType [" + contentType + "]"); if (DEBUG.DND) System.err.println("*** contentEncoding [" + contentEncoding + "]"); if (DEBUG.DND) System.err.println("*** contentLength [" + contentLength + "]"); setProperty("url.contentType", contentType); setProperty("url.contentEncoding", contentEncoding); if (contentLength >= 0) setProperty("url.contentLength", contentLength); //if (contentType.toLowerCase().startsWith("text/html") == false) { if (!isHTML()) { // we only currently handle HTML if (DEBUG.Enabled) System.err.println("*** contentType [" + contentType + "] not HTML; skipping title extraction"); return metaData; } if (DEBUG.DND) System.err.println("*** scanning for HTML meta-data..."); try { final BufferedInputStream bufStream = new BufferedInputStream(byteStream, maxSearchBytes); bufStream.mark(maxSearchBytes); final byte[] byteBuffer = new byte[maxSearchBytes]; int bytesRead = 0; int len = 0; // BufferedInputStream still won't read thru a block, so we need to allow // a few reads here to get thru a couple of blocks, so we can get up to // our maxbytes (e.g., a common return chunk count is 1448 bytes, presumably related to the MTU) do { int max = maxSearchBytes - bytesRead; len = bufStream.read(byteBuffer, bytesRead, max); System.out.println("*** read " + len); if (len > 0) bytesRead += len; else if (len < 0) break; } while (len > 0 && bytesRead < maxSearchBytes); if (DEBUG.DND) System.out.println("*** Got total chars: " + bytesRead); String html = new String(byteBuffer, 0, bytesRead); if (DEBUG.DND && DEBUG.META) System.out.println("*** HTML-STRING[" + html + "]"); // first, look for a content encoding, so we can search for and get the title // on a properly encoded character stream String charset = null; Matcher cm = Content_Charset_Regex.matcher(html); if (cm.lookingAt()) { charset = cm.group(1); if (DEBUG.DND) System.err.println("*** found HTML specified charset ["+charset+"]"); setProperty("charset", charset); } if (charset == null && contentEncoding != null) { if (DEBUG.DND||true) System.err.println("*** no charset found: using contentEncoding charset " + contentEncoding); charset = contentEncoding; } final String decodedHTML; if (charset != null) { bufStream.reset(); InputStreamReader decodedStream = new InputStreamReader(bufStream, charset); //InputStreamReader decodedStream = new InputStreamReader(new ByteArrayInputStream(byteBuffer), charset); if (true||DEBUG.DND) System.out.println("*** decoding bytes into characters with official encoding " + decodedStream.getEncoding()); setProperty("contentEncoding", decodedStream.getEncoding()); char[] decoded = new char[bytesRead]; int decodedChars = decodedStream.read(decoded); decodedStream.close(); if (true||DEBUG.DND) System.err.println("*** " + decodedChars + " characters decoded using " + charset); decodedHTML = new String(decoded, 0, decodedChars); } else decodedHTML = html; // we'll just have to go with the default platform charset... // these needed to be left open till the decodedStream was done, which // although it should never need to read beyond what's already buffered, // some internal java code has checks that make sure the underlying stream // isn't closed, even it it isn't used. byteStream.close(); bufStream.close(); Matcher m = HTML_Title_Regex.matcher(decodedHTML); if (m.lookingAt()) { String title = m.group(1); if (true||DEBUG.DND) System.err.println("*** found title ["+title+"]"); metaData.put("title", title.trim()); } } catch (Throwable e) { System.err.println("scrapeHTMLmetaData: " + e); if (DEBUG.DND) e.printStackTrace(); } if (DEBUG.DND || DEBUG.Enabled) System.err.println("*** scrapeHTMLmetaData returning [" + metaData + "]"); return metaData; } // private static void dumpBytes(String s) { // try { // dumpBytes(s.getBytes("UTF-8")); // } catch (Exception e) { // e.printStackTrace(); // } // } // private static void dumpBytes(byte[] bytes) { // for (int i = 0; i < bytes.length; i++) { // byte b = bytes[i]; // System.out.println("byte " + (i<10?" ":"") + i // + " (" + ((char)b) + ")" // + " " + pad(' ', 4, new Byte(b).toString()) // + " " + pad(' ', 2, Integer.toHexString( ((int)b) & 0xFF)) // + " " + pad('X', 8, toBinary(b)) // ); // } // } /* private static boolean isImage(final Resource r) { // doesn't make sense to check these now? Wouldn't the Images code will never have been // called unless we already considered this an image? // Oh, crap: backward compat with recently saved files? But still... if (r.getProperty("image.format") != null) // we already know this is an image return true; if (isImageMimeType(r.getProperty(Images.CONTENT_TYPE))) // check http contentType // || isImageMimeType(r.getProperty("format"))) // TODO: hack for FEDORA dublin-core mime-type return true; // todo: temporary hack for Osid2AssetResource w/Museum of Fine Arts, Boston // actually, it needs to set the spec for this to work //if (r.getProperty("largeImage") != null) //return true; String s = r.getSpec().toLowerCase(); if (s.endsWith(".gif") || s.endsWith(".jpg") || s.endsWith(".jpeg") || s.endsWith("=jpeg") // temporary hack for MFA until Resources become more Asset-like || s.endsWith(".png") || s.endsWith(".tif") || s.endsWith(".tiff") || s.endsWith(".fpx") || s.endsWith(".bmp") || s.endsWith(".ico") ) return true; return false; } */ /* public java.net.URL getImageURL() { // TODO: temporary hack Hack for FEDORA images, as the image URL is different // than the double-click URL. String imageURL = getProperty("Medium Image"); if (imageURL != null && getProperty("Identifier") != null) // Make sure it's FEDORA return makeURL(imageURL); else return asURL(); } */ // // Could create an Images.Thumbshot class that can be a recognized special image // // source (just the thumbshot URL), which getPreview can return, so ResourceIcon / // // PreviewPane can feed it to Images.getImage and get the async callback when it's // // loaded instead of having to fetch the thumbshot on the AWT EDT. (Also, Images // // can then manage caching the thumbshots, perhaps based on host only. Also may not // // want to bother caching those to disk in case of expiration). // private Image fetchThumbshot(URL url) // { // if (url == null || !"http".equals(url.getProtocol())) // return null; // final String thumbShotURL = "http://open.thumbshots.org/image.pxf?url=" + url; // final URL thumbShot = makeURL(thumbShotURL); // if (thumbShot == null) // return null; // // TODO: if we're currently on the AWT event thread, this should NOT run synchronously... // // We should spawn a thread for this. Otherwise, any delay in accessing thumbshots.org // // will result in the UI locking up until it responds with a result/error. // final boolean inUI_Thread = SwingUtilities.isEventDispatchThread(); // if (inUI_Thread) { // // 2007-11-05 SMF -- okay, this not safe, turning off for now: // if (DEBUG.Enabled) Log.debug("skipping thumbshot fetch in AWT EDT: " + thumbShot); // return null; // } // if (inUI_Thread) { // Log.warn("fetching thumbshot in AWT; may lock UI: " + thumbShot); // if (DEBUG.Enabled && DEBUG.META) Util.printStackTrace("fetchThumbshot " + thumbShot); // } else { // if (DEBUG.IO) Log.debug("attempting thumbshot: " + thumbShot); // } // Image image = null; // boolean gotError = false; // try { // image = ImageIO.read(thumbShot); // } catch (Throwable t) { // if (inUI_Thread) { // gotError = true; // Log.warn("fetching thumbshot in AWT; got error: " + thumbShot + "; " + t); // } // //if (DEBUG.Enabled) Util.printStackTrace(t, thumbShot.toString()); // } // if (inUI_Thread && !gotError) // Log.warn("fetching thumbshot in AWT; got: " + thumbShot); // if (image == null) { // if (DEBUG.WEBSHOTS) out("Didn't get a valid return from webshots : " + thumbShot); // } else if (image.getHeight(null) <= 1 || image.getWidth(null) <= 1) { // if (DEBUG.WEBSHOTS) out("This was a valid URL but there is no webshot available : " + thumbShot); // return null; // } // if (DEBUG.WEBSHOTS) out("Returning webshot image " + image); // return image; // } // public java.io.InputStream getByteStream() { // if (isImage()) // ; // return null; // } // private File cacheFile; // public void setCacheFile(File file) { // cacheFile = file; // Log.debug(this + "; cache file set to: " + cacheFile); // } // public File getCacheFile() { // return cacheFile; // } /* public void setPreview(Object preview) { // todo: ignored for now (Osid2AssetResource putting gunk here) //mPreview = preview; //out("Ignoring setPreview " + preview); } */ // TODO: calling with a different width/height only changes the size of // the existing icon, thus all who have reference to this will change! //public Icon getIcon(int width, int height) { //mIcon.setSize(width, height); /* public void setIcon(Icon i) { mIcon = i; } */ /* public void setPreview(JComponent preview) { this.preview = preview; } public JComponent getPreview() { return this.preview; } */ /* private JComponent viewer; public JComponent getAssetViewer(){ return null; } public void setAssetViewer(JComponent viewer){ this.viewer = viewer; } public Object getContent() throws IOException, MalformedURLException { tufts.Util.printStackTrace("DEPRECATED getContent " + this); if (isImage()) { return getImageIcon(); } else { final Object content = getContentData(); if (content instanceof ImageProducer) { // flag us as an image and/or pull that from contentType at other end of URL, // then re-try getContent to get the image. We can get here if someone // manually edits a resource string inside a LWImage, which will try // and pull it's image, but the resource doesn't know it is one yet. setProperty("url.contentType", "image/unknown"); return getImageIcon(); } else return content; } } private ImageIcon getImageIcon() throws IOException, MalformedURLException { URL u = toURL(); System.out.println(u + " fetching image"); ImageIcon imageIcon = new ImageIcon(u); System.out.println(u + " got image size " + imageIcon.getIconWidth() + "x" + imageIcon.getIconHeight()); setProperty("DEBUG.icon.width", imageIcon.getIconWidth()); setProperty("DEBUG.icon.height", imageIcon.getIconHeight()); return imageIcon; } public Object getContentData() throws IOException, MalformedURLException { // in the case of an HTML page, this just gets us a sun.net.www.protocol.http.HttpURLConnection$HttpInputStream, // -- the same as we get from openConnection() return toURL().getContent(); } public JComponent getPreview() { if (preview != null) return preview; try { // todo: cache the content type URL location = toURL(); URLConnection conn = location.openConnection(); if (conn == null) return null; String contentType = conn.getContentType(); // if inaccessable (e.g., offline) contentType will be null if (contentType == null) return null; if (DEBUG.Enabled) System.out.println(this + " getPreview: contentType=" + contentType); if (contentType.indexOf("text") >= 0) { /** JEditorPane editorPane = new JEditorPane(location); Thread.sleep(5); editorPane.setEditable(false); JButton button = new JButton("Hello"); editorPane.setSize(1000, 1000); Dimension size = editorPane.getSize(); BufferedImage image = new BufferedImage(size.width,size.height,BufferedImage.TYPE_INT_RGB); Graphics2D g2 = image.createGraphics(); g2.setBackground(Color.WHITE); g2.setColor(Color.BLACK); editorPane.printAll(g2); preview = new JLabel(new ImageIcon(image.getScaledInstance(75,75,Image.SCALE_FAST))); ** //javax.swing.filechooser.FileSystemView view = javax.swing.filechooser.FileSystemView.getFileSystemView(); //this.preview = new JLabel(view.getSystemIcon(File.createTempFile("temp",".html"))); // absurd to create a tmp file during object selection! Java doesn't give us the // real filesystem icon's anyway (check that on the PC -- this is OSX) } else if (contentType.indexOf("image") >= 0) { this.preview = new JLabel(new ImageIcon(location)); } } catch(Exception ex) { ex.printStackTrace(); } return preview; } */ // protected void out(String s) { // System.err.println(getClass().getName() + "@" + Integer.toHexString(hashCode()) + ": " + s); // } public static void main(String args[]) { String rs = args.length > 0 ? args[0] : "/"; VUE.parseArgs(args); DEBUG.Enabled = true; DEBUG.DND = true; URLResource r = (URLResource) Resource.instance(rs); System.out.println("Resource: " + r); //System.out.println("URL: " + r.asURL()); r.displayContent(); } }