/* * 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; /** * Abstract class for all "browse" based VUE data sources, sometimes called * "old style", before VUE had OSID integration for accessing data-sources via search. * * This class is for data-sources where all the content want's to be seen, based * on the configuration. E.g., a local directory, a list of user favorites, a remote FTP * site, an RSS feed, etc. * * @version $Revision: 1.16 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $ * @author rsaigal * @author sfraize */ import sun.net.www.protocol.file.FileURLConnection; import tufts.Util; import tufts.vue.DEBUG; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.io.*; import java.net.URL; import java.net.URLConnection; import javax.swing.JComponent; import org.xml.sax.InputSource; import edu.tufts.vue.ui.ConfigurationUI; public abstract class BrowseDataSource implements DataSource { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(BrowseDataSource.class); public static final String AUTHENTICATION_COOKIE_KEY = "url_authentication_cookie"; //protected static final String JIRA_SFRAIZE_COOKIE = "seraph.os.cookie=LkPlQkOlJlHkHiEpGiOiGjJjFi"; // TODO: test hack -- remove private String displayName = "(unconfigured)"; private String address; private String encoding; private String authenticationCookie; private String Id; private boolean isAutoConnect; private boolean isIncludedInSearch; private int publishMode; private volatile JComponent _viewer; // volatile should be overkill, but just in case private boolean isAvailable; private String hostName; private String guid; public BrowseDataSource() {} public BrowseDataSource(String name) { setDisplayName(name); } public synchronized String getGUID() { if (guid == null) setGUID(edu.tufts.vue.util.GUID.generate()); return guid; } public void setGUID(String s) { guid = s; } /** parameter block that can be used for generating XML in EditLibraryPanel to add fields to the data-source UI */ public static class ConfigField { public final String key; public final String title; public final String description; public final String value; public int uiControl = ConfigurationUI.SINGLE_LINE_CLEAR_TEXT_CONTROL; public int maxLen; public Vector<String> values; public ConfigField(String k, String t, String d, String v, int... type) { key = k; title = t; description = d; value = v; if (type.length > 0) uiControl = type[0]; } } // todo: to persist extra properties (e.g., authentication keys) add a getPropertyList for // castor that returns PropertyEntry's to persist extra key/values. Could use PropertyMap and // add a convert to list (URLResource just does this manually when requested for persistance by // castor) or add a util function. (or, could just hack it into RSS data source) /** * This handles the default properties "name" and "address" -- implementors should override * to add additional properties of their own. This is used by EditLibraryPanel to * pass the result of user property edits back into the VueDataSource. */ // todo: this is really just a bean interface: would be much easier to handle that way // using freely availble libraries (e.g., apache), and we could avoid yet enother // set of property key/value dispatch code. public void setConfiguration(java.util.Properties p) { String val = null; try { if ((val = p.getProperty("name")) != null) setDisplayName(val); } catch (Throwable t) { Log.error("val=" + val, t); } try { if ((val = p.getProperty("address")) != null) setAddress(val); } catch (Throwable t) { Log.error("val=" + val, t); } try { if ((val = p.getProperty(AUTHENTICATION_COOKIE_KEY)) != null) setAuthenticationCookie(val); } catch (Throwable t) { Log.error("val=" + val, t); } } public java.util.List<ConfigField> getConfigurationUIFields() { List<ConfigField> fields = new java.util.ArrayList(); fields.add(new ConfigField("address", VueResources.getString("dialog.address.label"), getTypeName() + " URL", getAddress())); fields.add(new ConfigField(AUTHENTICATION_COOKIE_KEY, VueResources.getString("dialog.authentication.label"), "Any required authentication cookie (optional)", getAuthenticationCookie())); return fields; } public String getTypeName() { return getClass().getSimpleName(); } public final void setAddress(String newAddress) { if (newAddress != null) newAddress = newAddress.trim(); if (DEBUG.DR) out("setAddress[" + newAddress + "]"); if (newAddress != null && !newAddress.equals(address)) { this.address = newAddress; // any time we change the address, rebuild the viewer unloadViewer(); java.net.URI uri; try { uri = new java.net.URI(newAddress); hostName = uri.getHost(); } catch (Throwable t) { hostName = null; } } } public final String getAddress() { return this.address; } public void setAuthenticationCookie(String s) { //if (DEBUG.DR) Log.debug("setAuthenticationCookie[" + s + "]"); if (s == authenticationCookie || (s != null && s.equals(authenticationCookie))) { return; } else { authenticationCookie = s; unloadViewer(); } } public String getAuthenticationCookie() { return authenticationCookie; } /** impl's may override to provide a count of the items in the view * @return -1 by defaut */ public int getCount() { return -1; } /** @return a host name if one can be found in the address, otherwise returns the address */ public String getAddressName() { if (hostName != null) return hostName; else return getAddress(); } public String getHostName() { return hostName; } public String getDisplayName() { return this.displayName; } public void setDisplayName(String name) { this.displayName = name; } /** * @return the JComponent that is current set to displays the content for this data source * Will return null until set. */ public final JComponent getResourceViewer() { return _viewer; } /** set the viewer that's been loaded */ // call from AWT only void setViewer(JComponent v) { if (DEBUG.Enabled && _viewer != v) out("setViewer " + tufts.vue.gui.GUI.name(v)); if (_viewer != null && _viewer != v) { // If we have an existing viewer, and it needs to know it's going away for // good, as opposed to just a removeNotify, which may be temporary, we // deliver a special message it can capture to detect this (e.g., it may be // running threads that need to be stopped, it may need to stop listenting // to events global events, etc). Log.debug("finalizing " + _viewer); _viewer.firePropertyChange(tufts.vue.gui.GUI.FINALIZE, false, true); } // this is this ONLY place _viewer should be set. _viewer = v; } // call from AWT only protected void unloadViewer() { //if (DEBUG.DR) out("unloadViewer"); if (mLoadThread != null) setLoadThread(null); if (_viewer != null) { if (DEBUG.DR) out("unloadViewer " + tufts.vue.gui.GUI.name(_viewer)); // a special message for viewers that may need to release resources, // remove listeners, terminate threads, etc. setViewer(null); } setAvailable(false); } private Thread mLoadThread; // call from AWT only void setLoadThread(Thread t) { if (DEBUG.Enabled) out("setLoadThread: " + t); if (mLoadThread != null && mLoadThread.isAlive()) { if (DEBUG.Enabled) Log.debug(this + "; setLoadThread: INTERRUPT " + mLoadThread); //if (DEBUG.Enabled) Log.warn(this + "; setLoadThread: FALLBACK-INTERRUPT " + mLoadThread); mLoadThread.interrupt(); } mLoadThread = t; } // call from AWT only Thread getLoadThread() { return mLoadThread; } // call from AWT only public boolean isLoading() { return mLoadThread != null; } boolean isAvailable() { return isAvailable; } void setAvailable(boolean t) { isAvailable = t; } /** * @return build a JComponent that displays the content for this data source * This will most likely NOT be called on the AWT thread, so it should * only build the component, and not add anything into any live on-screen * AWT component hierarchies. */ protected abstract JComponent buildResourceViewer(); public void setisAutoConnect() { this.isAutoConnect = false; } public int getPublishMode() { return this.publishMode; } public boolean isAutoConnect() { return this.isAutoConnect; } public void setAutoConnect(boolean b) { this.isAutoConnect = b; } public boolean isIncludedInSearch() { return this.isIncludedInSearch; } public void setIncludedInSearch(boolean included) { this.isIncludedInSearch = included; } /** @return a Reader for the data source, with an appropriately set encoding * Besides getting special characters to display correctly, using the right * encoding can be crucial for some XML streams, as the they may fail to * parse at all with an incorrect encoding. */ // SMF 2008-10-02: E.g. Craigslist XML streams use ISO-8859-1, which is provided in // HTML headers as "Content-Type: application/rss+xml; charset=ISO-8859-1", (tho not // in a special content-encoding header), and our current XML parser fails unless // the stream is read with this set: e.g.: [org.xml.sax.SAXParseException: Character // conversion error: "Unconvertible UTF-8 character beginning with 0x95" (line // number may be too low).] Actually, in this case it turns out that providing a // default InputStreamReader (encoding not specified) as opposed to a direct // InputStream from the URLConnection works, and the XML parser is presumably then // finding and handling the "<?xml version="1.0" encoding="ISO-8859-1"?>" line at // the top of the XML stream, but other code will find the automatic handling of the // encoding to be helpful. // We could also use a ROME API XmlReader(URLConnection) for handling // the input for known XML streams, which does it's own magic to figure out the encoding. // For more on the complexity of this issue, see: // http://diveintomark.org/archives/2004/02/13/xml-media-types protected Reader openReader() { // TODO: allow an encoding field to be specified on these data sources // TODO: this general stream providing functionality should be // something any Resource can do. // TODO: handle for local-file case URLConnection conn = openAddress(); String encoding = null; if (conn instanceof FileURLConnection) { BufferedReader bufferedReader = null; String xmlDeclaration = null; try { bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream())); xmlDeclaration = bufferedReader.readLine(); } catch (IOException e1) { e1.printStackTrace(); } if (xmlDeclaration !=null) { Pattern p = Pattern.compile(".*encoding=\"(.*?)\".*"); Matcher m = p.matcher(xmlDeclaration); m.find(); try { encoding = m.group(1); } catch(Exception e) { e.printStackTrace(); } finally { if (bufferedReader!=null) try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } conn = openAddress(); } } } else { encoding = conn.getContentEncoding(); } if (encoding == null) { String ct = conn.getContentType(); Log.debug("content-type[" + ct + "]"); if (ct != null) { int charsetIndex = ct.indexOf("charset="); if (charsetIndex >= 0) { encoding = ct.substring(charsetIndex + 8); Log.debug("charset[" + encoding + "]"); if (encoding.length() < 1) encoding = null; } } } else { Log.debug("content-encoding[" + encoding + "]"); } Reader reader = null; if (encoding != null) { try { reader = new InputStreamReader(conn.getInputStream(), encoding); } catch (Throwable t) { Log.warn("opening " + conn + " with encoding [" + encoding + "]", t); } } if (reader == null) { try { reader = new InputStreamReader(conn.getInputStream()); } catch (Throwable t) { throw new DataSourceException("Failed to get reader for stream " + conn, t); } } return reader; } protected org.xml.sax.InputSource openInput() { InputSource is = new InputSource(getAddress()); Reader reader = openReader(); // We don't use is.setEncoding(), as openReader will already have handled that is.setCharacterStream(reader); return is; } protected URLConnection openAddress() { String addressText = getAddress(); Log.debug("openAddress " + addressText); if (addressText.toLowerCase().startsWith("feed:")) addressText = "http:" + addressText.substring(5); URL address = null; try { address = new URL(addressText); } catch (Throwable t) { try { if (Util.isWindowsPlatform()) { String s = new File(addressText).toURI().toString(); address = new URL(s); } else address = new URL("file://" + addressText); } catch (Throwable t2) { address = null; } if (address == null) throw new DataSourceException("Bad address in " + getClass().getSimpleName(), t); } Map<String,List<String>> headers = null; try { if (DEBUG.Enabled) Log.debug("opening " + address); URLConnection conn = address.openConnection(); conn.setRequestProperty("User-Agent","Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.8) Gecko/20071008 Firefox/2.0.0.8"); if (getAuthenticationCookie() != null) conn.setRequestProperty("Cookie", getAuthenticationCookie()); if (DEBUG.Enabled) Log.debug("request-properties: " + conn.getRequestProperties()); conn.connect(); if (DEBUG.Enabled) { Log.debug("connected; fetching headers [" + conn + "]"); final StringBuilder buf = new StringBuilder(512); buf.append("headers [" + conn + "];\n"); headers = conn.getHeaderFields(); List<String> response = headers.get(null); if (response != null) buf.append(String.format("%20s: %s\n", "HTTP-RESPONSE", response)); for (Map.Entry<String,List<String>> e : headers.entrySet()) { if (e.getKey() != null) buf.append(String.format("%20s: %s\n", e.getKey(), e.getValue())); } Log.debug(buf); } return conn; } catch (java.io.IOException io) { throw new DataSourceException(null, io); } } @Override public final String toString() { return getClass().getSimpleName() + "[" + getDisplayName() + "; " + getAddress() + "]"; } private void out(String s) { Log.debug(getClass().getSimpleName() + "[" + getDisplayName() + "] " + s); } //private JPanel addDataSourcePanel; //private JPanel editDataSourcePanel; // public void setAddDataSourcePanel() { // this.addDataSourcePanel = new JPanel(); // } // public JComponent getAddDataSourcePanel(){ // return this.addDataSourcePanel; // } // public void setEditDataSourcePanel(){ // this.editDataSourcePanel = new JPanel(); // } // public JComponent getEditDataSourcePanel(){ // return this.editDataSourcePanel; // } }