/*
* 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;
// }
}