java.fedora.server.access.dissemination.DisseminationService Maven / Gradle / Ivy
Show all versions of fcrepo-client Show documentation
/*
* -----------------------------------------------------------------------------
*
* License and Copyright: The contents of this file are subject to the
* Apache 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.fedora-commons.org/licenses.
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
* the specific language governing rights and limitations under the License.
*
* The entire file consists of original code.
* Copyright © 2008 Fedora Commons, Inc.
*
Copyright © 2002-2007 The Rector and Visitors of the University of
* Virginia and Cornell University
* All rights reserved.
*
* -----------------------------------------------------------------------------
*/
package fedora.server.access.dissemination;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.Header;
import org.apache.log4j.Logger;
import fedora.common.Constants;
import fedora.common.http.WebClient;
import fedora.common.http.HttpInputStream;
import fedora.server.Context;
import fedora.server.Server;
import fedora.server.errors.DisseminationException;
import fedora.server.errors.DisseminationBindingInfoNotFoundException;
import fedora.server.errors.GeneralException;
import fedora.server.errors.HttpServiceNotFoundException;
import fedora.server.errors.InitializationException;
import fedora.server.errors.ServerException;
import fedora.server.errors.ServerInitializationException;
import fedora.server.security.Authorization;
import fedora.server.security.BackendPolicies;
import fedora.server.security.BackendSecurity;
import fedora.server.security.BackendSecuritySpec;
import fedora.server.storage.DOManager;
import fedora.server.storage.DOReader;
import fedora.server.storage.types.Datastream;
import fedora.server.storage.types.DatastreamMediation;
import fedora.server.storage.types.DisseminationBindingInfo;
import fedora.server.storage.types.MIMETypedStream;
import fedora.server.storage.types.MethodParmDef;
import fedora.server.storage.types.Property;
import fedora.server.utilities.DateUtility;
import fedora.server.utilities.ServerUtility;
/**
* Title: DisseminationService.java
* Description: A service for executing a dissemination given its
* binding information.
*
* @author [email protected]
* @version $Id: DisseminationService.java 6244 2007-10-31 18:54:34Z rwayland3 $
*/
public class DisseminationService
{
/** Logger for this class. */
private static final Logger LOG = Logger.getLogger(
DisseminationService.class.getName());
/** The Fedora Server instance */
private static Server s_server;
/** An instance of DO manager */
private static DOManager m_manager;
/** Signifies the special type of address location known as LOCAL.
* An address location of LOCAL implies that no remote host name is
* required for the address location and that the contents of the
* operation location are sufficient to execute the associated mechanism.
*/
private static final String LOCAL_ADDRESS_LOCATION = "LOCAL";
/** The expiration limit in minutes for removing entries from the database. */
private static int datastreamExpirationLimit = 0;
/** An incremental counter used to insure uniqueness of tempIDs used for
* datastream mediation.
*/
private static int counter = 0;
/** Datastream Mediation control flag. */
private static boolean doDatastreamMediation;
/** Configured Fedora server host */
private static String fedoraServerHost = null;
/** Configured Fedora server port */
private static String fedoraServerPort = null;
/** Configured Fedora redirect port */
private static String fedoraServerRedirectPort = null;
private static String fedoraHome = null;
private static BackendSecuritySpec m_beSS = null;
private static BackendSecurity m_beSecurity;
private static WebClient s_http;
/** Make sure we have a server instance for error logging purposes. */
static
{
try
{
fedoraHome = Constants.FEDORA_HOME;
if (fedoraHome == null)
{
throw new ServerInitializationException(
"[DisseminationService] Server failed to initialize: "
+ "FEDORA_HOME is undefined");
} else
{
s_server = Server.getInstance(new File(fedoraHome), false);
fedoraServerHost = s_server.getParameter("fedoraServerHost");
fedoraServerPort = s_server.getParameter("fedoraServerPort");
fedoraServerRedirectPort = s_server.getParameter("fedoraRedirectPort");
m_manager = (DOManager) s_server.getModule("fedora.server.storage.DOManager");
m_beSecurity = (BackendSecurity) s_server.getModule("fedora.server.security.BackendSecurity");
m_beSS = m_beSecurity.getBackendSecuritySpec();
String expireLimit = s_server.getParameter("datastreamExpirationLimit");
if (expireLimit == null || expireLimit.equalsIgnoreCase(""))
{
LOG.info("datastreamExpirationLimit unspecified; defaulting to "
+ "300 seconds");
datastreamExpirationLimit = 300;
} else
{
datastreamExpirationLimit = new Integer(expireLimit).intValue();
LOG.info("datastreamExpirationLimit=" + datastreamExpirationLimit);
}
String dsMediation =
s_server.getModule("fedora.server.access.Access").getParameter("doMediateDatastreams");
if (dsMediation == null || dsMediation.equalsIgnoreCase(""))
{
LOG.info("doMediateDatastreams unspecified; defaulting to false");
} else
{
doDatastreamMediation = new Boolean(dsMediation).booleanValue();
}
s_http = new WebClient();
s_http.USER_AGENT = "Fedora";
}
} catch (InitializationException ie)
{
LOG.error("Initialization error", ie);
}
}
/** The hashtable containing information required for datastream mediation. */
protected static Hashtable dsRegistry = new Hashtable(1000);
protected static Hashtable beSecurityHash = new Hashtable();
/**
* Constructs an instance of DisseminationService. Initializes two class
* variables that contain the IP address and port number of the Fedora server.
* The port number is obtained from the Fedora server config file and the IP
* address of the server is obtained dynamically. These variables are needed
* to perform the datastream proxy service for datastream requests.
*/
public DisseminationService()
{ }
/*
public void checkState(Context context, String state, String dsID, String PID)
throws ServerException
{
// Check Object State
if ( state.equalsIgnoreCase("D") &&
( context.get("canUseDeletedObject")==null
|| (!context.get("canUseDeletedObject").equals("true")) )
)
{
throw new GeneralException("The requested dissemination for data object \""+PID+"\" is no "
+ "longer available. One of its datastreams (dsID=\""+dsID+"\") has been flagged for DELETION "
+ "by the repository administrator. ");
} else if ( state.equalsIgnoreCase("I") &&
( context.get("canUseInactiveObject")==null
|| (!context.get("canUseInactiveObject").equals("true")) )
)
{
throw new GeneralException("The requested dissemination for data object \""+PID+"\" is no "
+ "longer available. One of its datastreams (dsID=\""+dsID+"\") has been flagged as INACTIVE "
+ "by the repository administrator. ");
}
}
*/
/**
* Assembles a dissemination given an instance of
* DisseminationBindingInfo
which has the dissemination-related
* information from the digital object and its associated Behavior
* Mechanism object.
*
* @param context The current context.
* @param PID The persistent identifier of the digital object.
* @param h_userParms A hashtable of user-supplied method parameters.
* @param dissBindInfoArray The associated dissemination binding information.
* @return A MIME-typed stream containing the result of the dissemination.
* @throws ServerException If unable to assemble the dissemination for any
* reason.
*/
public MIMETypedStream assembleDissemination(Context context, String PID,
Hashtable h_userParms, DisseminationBindingInfo[] dissBindInfoArray,
String bMechPid, String methodName)
throws ServerException
{
LOG.debug("Started assembling dissemination");
String dissURL = null;
String protocolType = null;
DisseminationBindingInfo dissBindInfo = null;
MIMETypedStream dissemination = null;
long initStartTime = new Date().getTime();
boolean isRedirect = false;
if (LOG.isDebugEnabled()) {
printBindingInfo(dissBindInfoArray);
}
if (dissBindInfoArray != null && dissBindInfoArray.length > 0)
{
String replaceString = null;
int numElements = dissBindInfoArray.length;
// Get row(s) of binding info and perform string substitution
// on DSBindingKey and method parameter values in WSDL
// Note: In case where more than one datastream matches the
// DSBindingKey or there are multiple DSBindingKeys for the
// method, multiple rows will be present; otherwise there is only
// a single row.
for (int i=0; i1) {
StringBuffer replaced=new StringBuffer();
replaced.append(parts[0]);
for (int x=1; x0) {
String key=parts[x].substring(0, rightParenPos);
String val=(String) h_userParms.get(key);
if (val!=null) {
// We have a match... so insert the urlencoded value.
try {
replaced.append(URLEncoder.encode(val, "UTF-8"));
} catch (UnsupportedEncodingException uee) {
// won't happen: java always supports UTF-8
}
if (rightParenPos= 0) {
dissURL = dissURL.replaceFirst(":"+fedoraServerPort+"/", ":"+fedoraServerRedirectPort+"/");
}
} else {
if (dissURL.startsWith("https:")) {
dissURL = dissURL.replaceFirst("https:", "http:");
}
if (dissURL.indexOf(":"+fedoraServerRedirectPort+"/") >= 0) {
dissURL = dissURL.replaceFirst(":"+fedoraServerRedirectPort+"/", ":"+fedoraServerPort+"/");
}
}
if (beServiceCallBasicAuth) {
if (dissURL.indexOf("getDS?") >= 0) {
dissURL = dissURL.replaceFirst("getDS\\?", "getDSAuthenticated\\?");
}
} else {
if (dissURL.indexOf("getDSAuthenticated?") >= 0) {
dissURL = dissURL.replaceFirst("getDSAuthenticated\\?", "getDS\\?");
}
}
}
*/
if (LOG.isDebugEnabled()) {
LOG.debug("******************getDisseminationContent beServiceRole: "+beServiceRole);
LOG.debug("******************getDisseminationContent beServiceCallBasicAuth: "+beServiceCallBasicAuth);
LOG.debug("******************getDisseminationContent beServiceCallSSL: "+beServiceCallSSL);
LOG.debug("******************getDisseminationContent beServiceCallUsername: "+beServiceCallUsername);
LOG.debug("******************getDisseminationContent beServiceCallPassword: "+beServiceCallPassword);
LOG.debug("******************getDisseminationContent dissURL: "+dissURL);
}
// Dispatch backend service URL request authenticating as necessary based on beSecurity configuration
dissemination = getDisseminationContent(dissURL, context, beServiceCallUsername, beServiceCallPassword);
}
} else if (protocolType.equalsIgnoreCase("soap"))
{
// FIXME!! future handling of soap bindings.
String message = "[DisseminationService] Protocol type: "
+ protocolType + "NOT yet implemented";
LOG.error(message);
throw new DisseminationException(message);
} else
{
String message = "[DisseminationService] Protocol type: "
+ protocolType + "NOT supported.";
LOG.error(message);
throw new DisseminationException(message);
}
} else
{
// DisseminationBindingInfo was empty so there was no information
// provided to construct a dissemination.
String message = "[DisseminationService] Dissemination Binding "+
"Info contained no data";
LOG.error(message);
throw new DisseminationBindingInfoNotFoundException(message);
}
return dissemination;
}
/**
* Datastream locations are considered privileged information by the
* Fedora repository. To prevent disclosing physical datastream locations
* to external mechanism services, a proxy is used to disguise the datastream
* locations. This method generates a temporary ID that maps to the
* physical datastream location and registers this information in a
* memory resident hashtable for subsequent resolution of the physical
* datastream location. The servlet DatastreamResolverServlet
* provides the proxy resolution service for datastreams.
*
* The format of the tempID is derived from java.sql.Timestamp
* with an arbitrary counter appended to the end to insure uniqueness. The
* syntax is of the form:
*
* YYYY-MM-DD HH:mm:ss.mmm:dddddd where
*
* - YYYY - year (1900-8099)
* - MM - month (01-12)
* - DD - day (01-31)
* - hh - hours (0-23)
* - mm - minutes (0-59)
* - ss - seconds (0-59)
* - mmm - milliseconds (0-999)
* - dddddd - incremental counter (0-999999)
*
*
*
* @param dsLocation The physical location of the datastream.
* @param dsControlGroupType The type of the datastream.
* @return A temporary ID used to reference the physical location of the
* specified datastream
* @throws ServerException If an error occurs in registering a datastream
* location.
*/
public String registerDatastreamLocation(String dsLocation,
String dsControlGroupType, String beServiceCallbackRole, String methodName) throws ServerException
{
String tempID = null;
Timestamp timeStamp = null;
if (counter > 999999) counter = 0;
long currentTime = new Timestamp(new Date().getTime()).getTime();
long expireLimit = currentTime -
(long)datastreamExpirationLimit*1000;
String dsMediatedServletPath = null;
String dsMediatedCallbackHost = null;
try
{
// Remove any datastream registrations that have expired.
// The expiration limit can be adjusted using the Fedora config parameter
// named "datastreamExpirationLimit" which is in seconds.
for ( Enumeration e = dsRegistry.keys(); e.hasMoreElements(); )
{
String key = (String)e.nextElement();
timeStamp = Timestamp.valueOf(extractTimestamp(key));
if (expireLimit > timeStamp.getTime())
{
dsRegistry.remove(key);
LOG.debug("DatastreamMediationKey removed from Hash: " + key);
}
}
// Register datastream.
if (tempID == null)
{
timeStamp = new Timestamp(new Date().getTime());
tempID = timeStamp.toString()+":"+counter++;
DatastreamMediation dm = new DatastreamMediation();
dm.mediatedDatastreamID = tempID;
dm.dsLocation = dsLocation;
dm.dsControlGroupType = dsControlGroupType;
dm.methodName = methodName;
// See if datastream reference is to fedora server itself or an external location.
// M and X type datastreams always reference fedora server. With E type datastreams
// we must examine URL to see if this is referencing a remote datastream or is
// simply a callback to the fedora server. If the reference is remote, then use
// the role of the backend service that will make a callback for this datastream.
// If the referenc s to the fedora server, use the special role of "fedoraInternalCall-1" to
// denote that the callback will come from the fedora server itself.
String beServiceRole = null;
if ( ServerUtility.isURLFedoraServer(dsLocation) ||
dsControlGroupType.equals("M") ||
dsControlGroupType.equals("X") ) {
beServiceRole = BackendPolicies.FEDORA_INTERNAL_CALL;
} else {
beServiceRole = beServiceCallbackRole;
}
// Store beSecurity info in hash
Hashtable beHash = m_beSS.getSecuritySpec(beServiceRole, methodName);
boolean beServiceCallbackBasicAuth = new Boolean((String) beHash.get("callbackBasicAuth")).booleanValue();
boolean beServiceCallBasicAuth = new Boolean((String) beHash.get("callBasicAuth")).booleanValue();
boolean beServiceCallbackSSL = new Boolean((String) beHash.get("callbackSSL")).booleanValue();
boolean beServiceCallSSL = new Boolean((String) beHash.get("callSSL")).booleanValue();
String beServiceCallUsername = (String) beHash.get("callUsername");
String beServiceCallPassword = (String) beHash.get("callPassword");
if (LOG.isDebugEnabled()) {
LOG.debug("******************Registering datastream dsLocation: "+dsLocation);
LOG.debug("******************Registering datastream dsControlGroupType: "+dsControlGroupType);
LOG.debug("******************Registering datastream beServiceRole: "+beServiceRole);
LOG.debug("******************Registering datastream beServiceCallbackBasicAuth: "+beServiceCallbackBasicAuth);
LOG.debug("******************Registering datastream beServiceCallBasicAuth: "+beServiceCallBasicAuth);
LOG.debug("******************Registering datastream beServiceCallbackSSL: "+beServiceCallbackSSL);
LOG.debug("******************Registering datastream beServiceCallSSL: "+beServiceCallSSL);
LOG.debug("******************Registering datastream beServiceCallUsername: "+beServiceCallUsername);
LOG.debug("******************Registering datastream beServiceCallPassword: "+beServiceCallPassword);
}
dm.callbackRole = beServiceRole;
dm.callUsername = beServiceCallUsername;
dm.callPassword = beServiceCallPassword;
dm.callbackBasicAuth = beServiceCallbackBasicAuth;
dm.callBasicAuth = beServiceCallBasicAuth;
dm.callbackSSL = beServiceCallbackSSL;
dm.callSSL = beServiceCallSSL;
dsRegistry.put(tempID, dm);
LOG.debug("DatastreammediationKey added to Hash: " + tempID);
}
} catch(Throwable th)
{
throw new DisseminationException("[DisseminationService] register"
+ "DatastreamLocation: "
+ "returned an error. The underlying error was a "
+ th.getClass().getName() + " The message "
+ "was \"" + th.getMessage() + "\" .");
}
// Replace the blank between date and time with the character "T".
return tempID.replaceAll(" ","T");
}
/**
* The tempID that is used for datastream mediation consists of a
* Timestamp
plus a counter appended to the end to insure uniqueness.
* This method is a utility method used to extract the Timestamp portion
* from the tempID by stripping off the arbitrary counter at the end of
* the string.
*
* @param tempID The tempID to be extracted.
* @return The extracted Timestamp value as a string.
*/
public String extractTimestamp(String tempID)
{
StringBuffer sb = new StringBuffer();
sb.append(tempID);
sb.replace(tempID.lastIndexOf(":"),tempID.length(),"");
return sb.toString();
}
/**
* Performs simple string replacement using regular expressions.
* All matching occurrences of the pattern string will be replaced in the
* input string by the replacement string.
*
* @param inputString The source string.
* @param patternString The regular expression pattern.
* @param replaceString The replacement string.
* @return The source string with substitutions.
*/
private String substituteString(String inputString, String patternString,
String replaceString)
{
Pattern pattern = Pattern.compile(patternString);
Matcher m = pattern.matcher(inputString);
return m.replaceAll(replaceString);
}
/**
*
Removes any optional userInputParms which remain in the dissemination
* URL. This occurs when a method has optional parameters and the user does
* not supply a value for one or more of the optional parameters. The result
* is a syntax similar to "parm=(PARM_BIND_KEY)". This method removes these
* non-supplied optional parameters from the string.
*
* @param dissURL String to be processed.
* @return An edited string with parameters removed where no value was
* specified for any optional parameters.
*/
private String stripParms(String dissURL)
{
String requestURI = dissURL.substring(0,dissURL.indexOf("?")+1);
String parmString = dissURL.substring(dissURL.indexOf("?")+1,dissURL.length());
String[] parms = parmString.split("&");
StringBuffer sb = new StringBuffer();
for (int i=0; iConverts the internal dsLocation used by managed and XML type datastreams
* to the corresponding Default Dissemination request that will return the
* datastream contents.
*
* @param internalDSLocation - dsLocation of the Managed or XML type datastream.
* @param PID - the persistent identifier of the digital object.
* @return - A URL corresponding to the Default Dissemination request for the
* specified datastream.
* @throws ServerException - If anything goes wrong during the conversion attempt.
*/
private String resolveInternalDSLocation(Context context, String internalDSLocation,
String PID, String callbackHost) throws ServerException
{
if (callbackHost == null || callbackHost.equals(""))
{
throw new DisseminationException("[DisseminationService] was unable to "
+ "resolve the base URL of the Fedora Server. The URL specified was: \""
+ callbackHost + "\". This information is required by the Dissemination Service.");
}
String[] s = internalDSLocation.split("\\+");
String dsLocation = null;
if (s.length == 3)
{
DOReader doReader = m_manager.getReader(Server.GLOBAL_CHOICE, context, PID);
Datastream d = (Datastream) doReader.getDatastream(s[1], s[2]);
dsLocation =
callbackHost
+"/fedora/get/"+s[0]+"/"+s[1]+"/"
+DateUtility.convertDateToString(d.DSCreateDT);
} else
{
String message = "[DisseminationService] An error has occurred. "
+ "The internal dsLocation: \"" + internalDSLocation + "\" is "
+ "not in the required format of: "
+ "\"doPID+DSID+DSVERSIONID\" .";
LOG.error(message);
throw new GeneralException(message);
}
LOG.debug("********** Resolving Internal Datastream dsLocation: "+dsLocation);
return dsLocation;
}
public static void printBindingInfo(DisseminationBindingInfo[] info) {
for (int i = 0; i < info.length; i++) {
LOG.debug("DisseminationBindingInfo[" + i + "]:");
LOG.debug(" DSBindKey : " + info[i].DSBindKey);
LOG.debug(" dsLocation : " + info[i].dsLocation);
LOG.debug(" dsControlGroupType : " + info[i].dsControlGroupType);
LOG.debug(" dsID : " + info[i].dsID);
LOG.debug(" dsVersionID : " + info[i].dsVersionID);
LOG.debug(" AddressLocation : " + info[i].AddressLocation);
LOG.debug(" OperationLocation : " + info[i].OperationLocation);
LOG.debug(" ProtocolType : " + info[i].ProtocolType);
LOG.debug(" dsState : " + info[i].dsState);
for (int j = 0; j < info[i].methodParms.length; j++) {
MethodParmDef def = info[i].methodParms[j];
LOG.debug(" MethodParamDef[" + j + "]:");
LOG.debug(" parmName : " + def.parmName);
LOG.debug(" parmDefaultValue : " + def.parmDefaultValue);
LOG.debug(" parmRequired : " + def.parmRequired);
LOG.debug(" parmLabel : " + def.parmLabel);
LOG.debug(" parmPassBy : " + def.parmPassBy);
for (int k = 0; k < def.parmDomainValues.length; k++) {
LOG.debug(" parmDomainValue : " + def.parmDomainValues[k]);
}
}
}
}
/**
* Get a MIMETypedStream for the given URL.
*
* If user or password are null
, basic authentication will
* not be attempted.
*/
private static MIMETypedStream get(String url,
String user,
String pass) throws GeneralException {
LOG.debug("DisseminationService.get(" + url + ")");
try {
HttpInputStream response = s_http.get(url, true, user, pass);
String mimeType = response.getResponseHeaderValue("Content-Type",
"text/plain");
Property[] headerArray = toPropertyArray(
response.getResponseHeaders());
return new MIMETypedStream(mimeType, response, headerArray);
} catch (Exception e) {
throw new GeneralException("Error getting " + url, e);
}
}
/**
* Convert the given HTTP Headers
to an array of
* Property
objects.
*/
private static Property[] toPropertyArray(Header[] headers) {
Property[] props = new Property[headers.length];
for (int i = 0; i < headers.length; i++) {
props[i] = new Property();
props[i].name = headers[i].getName();
props[i].value = headers[i].getValue();
}
return props;
}
/**
* A method that reads the contents of the specified URL and returns the
* result as a MIMETypedStream
*
* @param url The URL of the external content.
* @return A MIME-typed stream.
* @throws HttpServiceNotFoundException If the URL connection could not
* be established.
*/
public MIMETypedStream getDisseminationContent(String url, Context context, String user, String pass)
throws GeneralException, HttpServiceNotFoundException {
return get(url, user, pass);
}
}