tufts.vue.URLResource.java Source code

Java tutorial

Introduction

Here is the source code for tufts.vue.URLResource.java

Source

/*
* 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>&nbsp;";
    //             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();
    }

}