/**
* Copyright 2008 The University of North Carolina at Chapel Hill
*
* Licensed under 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.apache.org/licenses/LICENSE-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 fedorax.server.module.storage;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Map;
import javax.activation.MimetypesFileTypeMap;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import org.apache.http.Header;
import org.fcrepo.common.http.HttpInputStream;
import org.fcrepo.common.http.WebClient;
import org.fcrepo.server.Module;
import org.fcrepo.server.Server;
import org.fcrepo.server.errors.GeneralException;
import org.fcrepo.server.errors.HttpServiceNotFoundException;
import org.fcrepo.server.errors.ModuleInitializationException;
import org.fcrepo.server.errors.ValidationException;
import org.fcrepo.server.errors.authorization.AuthzException;
import org.fcrepo.server.security.Authorization;
import org.fcrepo.server.security.BackendPolicies;
import org.fcrepo.server.security.BackendSecurity;
import org.fcrepo.server.security.BackendSecuritySpec;
import org.fcrepo.server.storage.ContentManagerParams;
import org.fcrepo.server.storage.ExternalContentManager;
import org.fcrepo.server.storage.types.MIMETypedStream;
import org.fcrepo.server.storage.types.Property;
import org.fcrepo.server.utilities.ServerUtility;
import org.fcrepo.server.validation.ValidationUtility;
import org.irods.jargon.core.connection.IRODSAccount;
import org.irods.jargon.core.exception.JargonException;
import org.irods.jargon.core.pub.IRODSFileSystem;
import org.irods.jargon.core.pub.io.IRODSFile;
import org.irods.jargon.core.pub.io.IRODSFileFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import edu.unc.lib.staging.Stages;
import edu.unc.lib.staging.StagingArea;
import edu.unc.lib.staging.StagingException;
import fedorax.server.module.storage.lowlevel.irods.IrodsLowlevelStorageModule;
/**
* @author Gregory Jansen
*
*/
public class IrodsExternalContentManager extends Module implements
ExternalContentManager {
private static final Logger LOG = LoggerFactory
.getLogger(IrodsExternalContentManager.class);
static {
// Register IRODS URL Protocol Handler (see metadata project)
// by making a static reference to the class that loads it
@SuppressWarnings("unused")
String foo = IrodsLowlevelStorageModule.REGISTRY_NAME;
}
/**
* Stages configuration JSON file
*/
private File stagesConfiguration;
public File getStagesConfiguration() {
return stagesConfiguration;
}
public void setStagesConfiguration(File stagesConfiguration) {
this.stagesConfiguration = stagesConfiguration;
}
private Stages stages;
public Stages getStages() {
return this.stages;
}
// injected properties
private IRODSAccount irodsAccount;
private int irodsReadBufferSize;
public IRODSAccount getIrodsAccount() {
return irodsAccount;
}
public void setIrodsAccount(IRODSAccount irodsAccount) {
this.irodsAccount = irodsAccount;
}
public int getIrodsReadBufferSize() {
return irodsReadBufferSize;
}
public void setIrodsReadBufferSize(int irodsReadBufferSize) {
this.irodsReadBufferSize = irodsReadBufferSize;
}
// runtime stats
int connectionsUsed = 0;
int currentConnectionUsage = 0;
boolean reuseConnections = false;
// constants
private static final String DEFAULT_MIMETYPE = "text/plain";
// initialized properties
private String fedoraServerPort = "80";
private String fedoraServerRedirectPort = "443";
private WebClient m_http;
/**
* @param moduleParameters
* @param server
* @param role
* @throws ModuleInitializationException
*/
public IrodsExternalContentManager(Map<String, String> moduleParameters,
Server server, String role) throws ModuleInitializationException {
super(moduleParameters, server, role);
}
/**
* Initializes the Module based on configuration parameters. The
* implementation of this method is dependent on the schema used to define
* the parameter names for the role of
* <code>fedora.server.storage.DefaultExternalContentManager</code>.
*
* @throws ModuleInitializationException
* If initialization values are invalid or initialization fails
* for some other reason.
*/
@Override
public void initModule() throws ModuleInitializationException {
try {
Server s_server = getServer();
if (s_server != null) {
fedoraServerPort = s_server.getParameter("fedoraServerPort");
fedoraServerRedirectPort = s_server
.getParameter("fedoraRedirectPort");
}
m_http = new WebClient();
StringBuilder sb = new StringBuilder();
try(BufferedReader r = new BufferedReader(new FileReader(this.stagesConfiguration))) {
for (String line = r.readLine(); line != null; line = r
.readLine()) {
sb.append(line).append('\n');
}
}
LOG.debug("local staging config:\n"+sb.toString());
this.stages = new Stages(sb.toString(), new IRODSStageResolver(irodsAccount));
for (StagingArea s : this.stages.getAllAreas().values()) {
if (!s.isConnected()) {
this.stages.connect(s.getURI());
if (!s.isConnected()) {
LOG.warn("Cannot connect to staging area: "
+ s.getURI());
}
}
}
// TODO register Stages as MBean
//ObjectName name = new ObjectName("edu.unc.lib.cdr:type=Stages");
//MBeanServer mbs = this.getMBeanServer();
//mbs.registerMBean(this.stages, name);
} catch (Throwable th) {
th.printStackTrace();
throw new ModuleInitializationException(
"[IrodsExternalContentManager] "
+ "An external content manager "
+ "could not be instantiated. The underlying error was a "
+ th.getClass() + "The message was \""
+ th.getMessage() + "\".", getRole());
}
}
@SuppressWarnings("unused")
private MBeanServer getMBeanServer() {
MBeanServer mbserver = null;
ArrayList<MBeanServer> mbservers = MBeanServerFactory
.findMBeanServer(null);
if (mbservers.size() > 0) {
mbserver = (MBeanServer) mbservers.get(0);
}
if (mbserver != null) {
System.out.println("Found our MBean server");
} else {
mbserver = MBeanServerFactory.createMBeanServer();
}
return mbserver;
}
/*
* Retrieves the external content. Currently the protocols <code>file</code>
* and <code>http[s]</code> are supported.
*
* @see
* fedora.server.storage.ExternalContentManager#getExternalContent(fedora
* .server.storage.ContentManagerParams)
*/
public MIMETypedStream getExternalContent(ContentManagerParams params)
throws GeneralException, HttpServiceNotFoundException {
LOG.debug("in getExternalContent(), url=" + params.getUrl());
String protocol = params.getProtocol();
URI uri = URI.create(params.getUrl());
boolean staged = false;
try {
LOG.debug("manifestURI: "+uri);
LOG.debug("stages: "+this.stages);
LOG.debug("stages size: "+this.stages.getAllAreas().size());
URI storageURI = this.stages.getStorageURI(uri);
LOG.debug("storageURI: "+storageURI);
staged = true;
protocol = storageURI.getScheme();
uri = storageURI;
} catch(StagingException e) {
LOG.warn("Exception throw resolving local URL", e);
}
LOG.debug("protocol is " + protocol + ", uri is " + uri);
if (protocol == null && uri.toString().startsWith("irods://")) {
return getFromIrods(uri, params.getMimeType());
} else if (protocol == null || protocol.equals("file")) {
return getFromFilesystem(uri, params.getMimeType(), staged,
params);
} else if (protocol.equals("http") || protocol.equals("https")) {
try {
return getFromWeb(params);
} catch(ModuleInitializationException e) {
throw new GeneralException(e.getMessage()+"("+params.getUrl()+")", e);
}
} else if (protocol.equals("irods")) {
return getFromIrods(uri, params.getMimeType());
}
throw new GeneralException(
"protocol for retrieval of external content not supported. URL: "
+ params.getUrl());
}
/**
* @param params
* @return
*/
private MIMETypedStream getFromIrods(URI uri, String mimeType)
throws HttpServiceNotFoundException, GeneralException {
try {
LOG.debug("uri: "+uri);
IRODSFileFactory ff = IRODSFileSystem.instance()
.getIRODSFileFactory(irodsAccount);
IRODSFile file = ff.instanceIRODSFile(URLDecoder.decode(
uri.getRawPath(), "UTF-8"));
InputStream result = ff.instanceIRODSFileInputStream(file);
final long start = System.currentTimeMillis();
result = new BufferedInputStream(result, this.irodsReadBufferSize) {
int bytes = 0;
@Override
public void close() throws IOException {
if (LOG.isInfoEnabled()) {
long time = System.currentTimeMillis() - start;
if (time > 0) {
LOG.info("closed irods stream: " + bytes
+ " bytes at " + (bytes / time) + " kb/sec");
}
}
super.close();
}
@Override
public synchronized int read() throws IOException {
bytes++;
return super.read();
}
@Override
public synchronized int read(byte[] b, int off, int len)
throws IOException {
bytes = bytes + len;
return super.read(b, off, len);
}
};
// if mimeType was not given, try to determine it automatically
if (mimeType == null || mimeType.equalsIgnoreCase("")) {
String irodsFilename = file.getName();
if (irodsFilename != null) {
mimeType = new MimetypesFileTypeMap()
.getContentType(irodsFilename);
}
if (mimeType == null || mimeType.equalsIgnoreCase("")) {
mimeType = DEFAULT_MIMETYPE;
}
}
return new MIMETypedStream(mimeType, result,
getPropertyArray(mimeType), file.length());
/*
* } catch (AuthzException ae) { LOG.error(ae.getMessage(), ae);
* throw new
* HttpServiceNotFoundException("Policy blocked datastream resolution"
* , ae); } catch (GeneralException me) { LOG.error(me.getMessage(),
* me); throw me; }
*/
} catch (JargonException e) {
throw new GeneralException("Problem getting iRODS input stream", e);
} catch (Throwable th) {
th.printStackTrace(System.err);
// catch anything but generalexception
LOG.error(th.getMessage(), th);
throw new HttpServiceNotFoundException(
"[FileExternalContentManager] "
+ "returned an error. The underlying error was a "
+ th.getClass().getName() + " The message "
+ "was \"" + th.getMessage() + "\" . ", th);
}
}
/**
* @param mimeType
* @return
*/
private Property[] getPropertyArray(String mimeType) {
Property[] props = new Property[1];
Property ctype = new Property("Content-Type", mimeType);
props[0] = ctype;
return props;
}
/**
* Get a MIMETypedStream for the given URL. If user or password are
* <code>null</code>, basic authentication will not be attempted.
*/
private MIMETypedStream get(String url, String user, String pass,
String knownMimeType) throws GeneralException {
LOG.debug("DefaultExternalContentManager.get(" + url + ")");
try {
HttpInputStream response = m_http.get(url, true, user, pass);
String mimeType = response.getResponseHeaderValue("Content-Type",
knownMimeType);
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 <code>Headers</code> to an array of
* <code>Property</code> 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;
}
/**
* Creates a property array out of the MIME type and the length of the
* provided file.
*
* @param file
* the file containing the content.
* @return an array of properties containing content-length and
* content-type.
*/
private static Property[] getPropertyArray(File file, String mimeType) {
Property[] props = new Property[2];
Property clen = new Property("Content-Length", Long.toString(file
.length()));
Property ctype = new Property("Content-Type", mimeType);
props[0] = clen;
props[1] = ctype;
return props;
}
/**
* Get a MIMETypedStream for the given URL. If user or password are
* <code>null</code>, basic authentication will not be attempted.
*
* @param params
* @return
* @throws HttpServiceNotFoundException
* @throws GeneralException
*/
private MIMETypedStream getFromFilesystem(URI uri, String mimeType,
boolean staged, ContentManagerParams params)
throws HttpServiceNotFoundException, GeneralException {
LOG.debug("in getFile(), url=" + uri);
try {
File cFile = new File(uri.getPath()).getCanonicalFile();
// security check, staged files are in known locations
if (!staged) {
URI cURI = cFile.toURI();
LOG.info("Checking resolution security on " + cURI);
Authorization authModule = (Authorization) getServer()
.getModule("fedora.server.security.Authorization");
if (authModule == null) {
throw new GeneralException(
"Missing required Authorization module");
}
authModule.enforceRetrieveFile(params.getContext(),
cURI.toString());
}
// if mimeType was not given, try to determine it automatically
if (mimeType == null || mimeType.equalsIgnoreCase("")) {
mimeType = determineMimeType(cFile);
}
InputStream in = new FileInputStream(cFile);
return new MIMETypedStream(mimeType, in,
getPropertyArray(cFile, mimeType));
} catch (AuthzException ae) {
LOG.error(ae.getMessage(), ae);
throw new HttpServiceNotFoundException(
"Policy blocked datastream resolution", ae);
} catch (GeneralException me) {
LOG.error(me.getMessage(), me);
throw me;
} catch (Throwable th) {
th.printStackTrace(System.err);
// catch anything but generalexception
LOG.error(th.getMessage(), th);
throw new HttpServiceNotFoundException(
"[FileExternalContentManager] "
+ "returned an error. The underlying error was a "
+ th.getClass().getName() + " The message "
+ "was \"" + th.getMessage() + "\" . ", th);
}
}
/**
* Retrieves external content via http or https.
*
* @param url
* The url pointing to the content.
* @param context
* The Map containing parameters.
* @param mimeType
* The default MIME type to be used in case no MIME type can be
* detected.
* @return A MIMETypedStream
* @throws ModuleInitializationException
* @throws GeneralException
*/
private MIMETypedStream getFromWeb(ContentManagerParams params)
throws ModuleInitializationException, GeneralException {
String username = params.getUsername();
String password = params.getPassword();
boolean backendSSL = false;
String url = params.getUrl();
if (ServerUtility.isURLFedoraServer(url) && !params.isBypassBackend()) {
BackendSecuritySpec m_beSS;
BackendSecurity m_beSecurity = (BackendSecurity) getServer()
.getModule("fedora.server.security.BackendSecurity");
try {
m_beSS = m_beSecurity.getBackendSecuritySpec();
} catch (Exception e) {
throw new ModuleInitializationException(
"Can't intitialize BackendSecurity module (in default access) from Server.getModule",
getRole());
}
Hashtable<String, String> beHash = m_beSS
.getSecuritySpec(BackendPolicies.FEDORA_INTERNAL_CALL);
username = beHash.get("callUsername");
password = beHash.get("callPassword");
backendSSL = new Boolean(beHash.get("callSSL")).booleanValue();
if (backendSSL) {
if (params.getProtocol().equals("http:")) {
url = url.replaceFirst("http:", "https:");
}
url = url.replaceFirst(":" + fedoraServerPort + "/", ":"
+ fedoraServerRedirectPort + "/");
}
if (LOG.isDebugEnabled()) {
LOG.debug("************************* backendUsername: "
+ username + " backendPassword: " + password
+ " backendSSL: " + backendSSL);
LOG.debug("************************* doAuthnGetURL: " + url);
}
}
return get(url, username, password, params.getMimeType());
}
/**
* Determines the mime type of a given file
*
* @param file
* for which the mime type needs to be detected
* @return the detected mime type
*/
private String determineMimeType(File file) {
String mimeType = new MimetypesFileTypeMap().getContentType(file);
// if mimeType detection failed, fall back to the default
if (mimeType == null || mimeType.equalsIgnoreCase("")) {
mimeType = DEFAULT_MIMETYPE;
}
return mimeType;
}
@Override
public void postInitModule() throws ModuleInitializationException {
super.postInitModule();
// check if Fedora is patched via ValidateURL utility thing
try {
ValidationUtility.validateURL(
"irods://example.com:1247/fooZone/home/foo", "M");
} catch (ValidationException e1) {
String msg = "Fedora Server is not patched to support the IrodsExternalContentManager";
LOG.error(msg, e1);
throw new ModuleInitializationException(msg,
"fedora.server.storage.ExternalContentManager", e1);
}
}
}