com.googlecode.fascinator.storage.fedora.Fedora3.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.fascinator.storage.fedora.Fedora3.java

Source

/* 
 * The Fascinator - Fedora Commons 3.x storage plugin
 * Copyright (C) 2009-2011 University of Southern Queensland
 * Copyright (C) 2011 Queensland Cyber Infrastructure Foundation (http://www.qcif.edu.au/)
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package com.googlecode.fascinator.storage.fedora;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.axis.types.NonNegativeInteger;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.fcrepo.client.FedoraClient;
import org.fcrepo.server.access.FedoraAPIA;
import org.fcrepo.server.management.FedoraAPIM;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.googlecode.fascinator.api.storage.StorageException;
import com.googlecode.fascinator.common.BasicHttpClient;
import com.googlecode.fascinator.common.JsonSimpleConfig;

/**
 * A private utility class to wrap Fedora connectivity and save repeated
 * instantiations across the package. Many methods/properties here are default
 * scoped to package-private so Payloads, DigitalObjects and the top-level
 * Storage class all get access.
 * 
 * @author Greg Pendlebury
 */
public class Fedora3 {
    /** Default Fedora base URL **/
    private static final String DEFAULT_URL = "http://localhost:8080/fedora/";

    /** Default Fedora namespace **/
    private static final String DEFAULT_NAMESPACE = "uuid";

    /** Test PID to retrieve from the server **/
    private static final String FEDORA_TEST_PID = "fedora-system:FedoraObject-3.0";

    /** Logger */
    private static Logger log = LoggerFactory.getLogger(Fedora3.class);

    /** System Config */
    private static JsonSimpleConfig systemConfig;

    /** Fedora - Client */
    private static FedoraClient fedoraClient;

    /** Fedora - API-A */
    private static FedoraAPIA accessApi;

    /** Fedora - API-M */
    private static FedoraAPIM managementApi;

    /** Fedora - Base URL */
    private static String fedoraUrl;

    /** Fedora - Get URL */
    private static String fedoraGetUrl;

    /** Fedora - Username */
    private static String fedoraUsername;

    /** Fedora - Password */
    private static String fedoraPassword;

    /** Fedora - Namespace */
    private static String fedoraNamespace;

    /** Fedora - Connection timeout */
    private static int fedoraTimeout;

    /** Fedora - Server version */
    private static String fedoraVersion;

    /** Fascinator HTTP Client */
    private static BasicHttpClient http;

    /** Open HTTP Connections */
    private static Map<String, List<GetMethod>> connections;

    /**
     * Public init method for File based configuration.
     * 
     * @param jsonFile The File containing JSON configuration
     * @throws StorageException if any errors occur
     */
    static void init(File jsonFile) throws StorageException {
        try {
            systemConfig = new JsonSimpleConfig(jsonFile);
            init();
        } catch (IOException ioe) {
            throw new StorageException("Failed to read file configuration!", ioe);
        }
    }

    /**
     * Public init method for String based configuration.
     * 
     * @param jsonString The String containing JSON configuration
     * @throws StorageException if any errors occur
     */
    static void init(String jsonString) throws StorageException {
        try {
            systemConfig = new JsonSimpleConfig(jsonString);
            init();
        } catch (IOException ioe) {
            throw new StorageException("Failed to read string configuration!", ioe);
        }
    }

    /**
     * Constructor
     * 
     * @throws StorageException if any errors occur
     */
    private static void init() throws StorageException {
        // Don't instantiate twice
        if (fedoraClient != null) {
            return;
        }

        // Grab all our information from config
        fedoraUrl = systemConfig.getString(DEFAULT_URL, "storage", "fedora3", "url");
        fedoraUsername = systemConfig.getString(null, "storage", "fedora3", "username");
        fedoraPassword = systemConfig.getString(null, "storage", "fedora3", "password");
        fedoraNamespace = systemConfig.getString(DEFAULT_NAMESPACE, "storage", "fedora3", "namespace");
        fedoraTimeout = systemConfig.getInteger(15, "storage", "fedora3", "timeout");
        if (fedoraUrl == null || fedoraNamespace == null || fedoraUsername == null || fedoraPassword == null) {
            throw new StorageException("Fedora Storage:" + " Valid Fedora configuration is mising!");
        }

        // Sort out our base URL and HTTP client
        if (!fedoraUrl.endsWith("/")) {
            fedoraUrl += "/";
        }
        fedoraGetUrl = fedoraUrl + "get/";
        http = new BasicHttpClient(fedoraUrl);
        http.authenticate(fedoraUsername, fedoraPassword);
        connections = new HashMap<String, List<GetMethod>>();
        // Will throw the StorageException for us if there's something wrong
        fedoraConnect();
    }

    /**
     * Establish a connection to Fedora's management API (API-M) to confirm
     * credentials, then return the instantiated fedora client used to connect.
     * 
     * @return FedoraClient : The client used to connect to the API
     * @throws StorageException if there was an error
     */
    private static FedoraClient fedoraConnect() throws StorageException {
        if (fedoraClient != null) {
            return fedoraClient;
        }

        try {
            // Connect to the server
            fedoraClient = new FedoraClient(fedoraUrl, fedoraUsername, fedoraPassword);
            fedoraClient.SOCKET_TIMEOUT_SECONDS = fedoraTimeout;

            // Because this is a new connection we're going
            // to do some additional work (and logging)
            log.info("Connected to FEDORA : '{}'", fedoraUrl);
            // Make sure we can get the server version
            fedoraVersion = fedoraClient.getServerVersion();
            log.info("FEDORA version: '{}'", fedoraVersion);
            // And that we have appropriate access to the management API
            managementApi = fedoraClient.getAPIM();
            log.info("API-M access testing; {} second timeout", fedoraTimeout);

            // Version cutout
            if (!fedoraVersion.startsWith("3.")) {
                throw new StorageException("Error; this plugin is designed" + " to work with Fedora versions 3.x");
            }

            // Version 3.x credentials test..
            byte[] data = managementApi.getObjectXML(FEDORA_TEST_PID);
            if (data != null && data.length > 0) {
                log.info("Access checked: '{}' = {} bytes", FEDORA_TEST_PID, data.length);
            } else {
                throw new StorageException("Error; could not retrieve " + FEDORA_TEST_PID);
            }
        } catch (MalformedURLException ex) {
            throw new StorageException("Fedora Storage:" + " Server URL is Invalid (?) : ", ex);
        } catch (IOException ex) {
            throw new StorageException("Fedora Storage:" + " Error connecting to Fedora! : ", ex);
        } catch (Exception ex) {
            throw new StorageException("Fedora Storage:" + " Error accesing management API! : ", ex);
        }
        return fedoraClient;
    }

    /**
     * Package-private version check of the connected server.
     * 
     * @return String The Fedora Server's version
     */
    static String getVersion() {
        return fedoraVersion;
    }

    /**
     * Package-private 'getter' method for the base Fedora Client.
     * 
     * @return FedoraClient The Fedora Client Object
     * @throws StorageException if any errors occur
     */
    static FedoraClient getClient() throws StorageException {
        return fedoraConnect();
    }

    /**
     * Package-private 'getter' method for the management API.
     * 
     * @return FedoraAPIM The Fedora API-M interface
     * @throws StorageException if any errors occur
     */
    static FedoraAPIM getApiM() throws StorageException {
        if (managementApi == null) {
            getClient();
            if (managementApi == null) {
                throw new StorageException("Fedora Storage:" + " Unknown problem accessing API-M!");
            }
        }
        return managementApi;
    }

    /**
     * Package-private 'getter' method for the access API.
     * 
     * @return FedoraAPIM The Fedora API-M interface
     * @throws StorageException if any errors occur
     */
    static FedoraAPIA getApiA() throws StorageException {
        if (accessApi == null) {
            FedoraClient client = getClient();
            try {
                accessApi = client.getAPIA();
            } catch (Exception ex) {
                throw new StorageException("Fedora Storage:" + " Error accesing standard API! : ", ex);
            }
            if (accessApi == null) {
                throw new StorageException("Fedora Storage:" + " Unknown problem standard API-M!");
            }
        }
        return accessApi;
    }

    /**
     * Trivial 'getter' for retrieving the configured namespace.
     * 
     * @return String The configured namespace Fascinator is using in Fedora
     */
    static String namespace() {
        return fedoraNamespace;
    }

    /**
     * Wrapper for creating an Axis Non-negative Integer object from a String.
     * 
     * @param number the number (as a String) to wrap in an object
     * @return NonNegativeInteger The instantiated Axis NonNegativeInteger
     */
    static NonNegativeInteger axisNum(String number) {
        return new NonNegativeInteger(number);
    }

    /**
     * Wrapper for creating an Axis Non-negative Integer object from an int.
     * 
     * @param number the number (as an int) to wrap in an object
     * @return NonNegativeInteger The instantiated Axis NonNegativeInteger
     */
    static NonNegativeInteger axisNum(int number) {
        return new NonNegativeInteger(String.valueOf(number));
    }

    /**
     * Get InputStream of a Datastream via an authenticated web requested.
     * 
     * @param fedoraPid The fedora PID containing the datastream
     * @param dsId The datastream's ID
     * @return InputStream An input stream for the datastream
     * @throws IOException if an errors occur
     */
    static InputStream getStream(String fedoraPid, String dsId) throws IOException {
        // log.debug("getStream({}, {})", fedoraPid, dsId);
        String key = fedoraPid + "/" + dsId;
        String url = buildGetUrl(fedoraPid, dsId);
        if (url == null) {
            throw new IOException("Unable to build URL for IDs!");
        }

        GetMethod method = new GetMethod(url);
        int status = http.executeMethod(method, true);
        if (status == HttpStatus.SC_OK) {
            InputStream response = method.getResponseBodyAsStream();
            // Cache our connection
            if (!connections.containsKey(key)) {
                connections.put(key, new ArrayList<GetMethod>());
            }
            connections.get(key).add(method);
            return response;
        }
        method.releaseConnection();
        return null;
    }

    /**
     * Release an open HTTP connection that may be held for this combination of
     * PID and DSID.
     * 
     * @param fedoraPid The fedora PID containing the datastream
     * @param dsId The datastream's ID
     */
    static void release(String fedoraPid, String dsId) {
        // log.debug("release({}, {})", fedoraPid, dsId);

        /*
         * This idea is worthwhile, but at this stage seems impractical.
         * We need some way of defining a key that is targetable to an
         * individual connection, not just to a payload.
         * 
         * The API needs to be changed to accomodate this sort of thing. It
         * would most likely involve return a result object that provides
         * access to both the stream and a token that can be used on return.
         * 
         * =============
         * TODO: Another option
         * http://hc.apache.org/httpclient-3.x/apidocs/org/apache/commons/httpclient/HttpMethod.html#getResponseBodyAsStream%28%29
         * 
         * "... null may be returned ... if this method was called previously
         * and the resulting stream was closed."
         * =====
         * 
         * So, relying on the above logic You could keep lists of connections
         * against each key, and each close() call against the key would check
         * for null inputstreams per connection, closing those that it finds...
         * 
         * This idea minimises opened connections hanging around, but doesn't
         * address closing the actual InputStreams... it only closes connections
         * where the user remembered to close the InputStream themselves.
         * 
         */

        String key = fedoraPid + "/" + dsId;
        if (connections.containsKey(key)) {
            // Prepare
            List<Integer> toClose = new ArrayList<Integer>();
            List<GetMethod> toTest = connections.get(key);
            Integer limit = toTest.size();
            // Loop through all connections
            for (Integer i = 0; i < limit; i++) {
                boolean close = false;
                try {
                    InputStream in = toTest.get(i).getResponseBodyAsStream();
                    // Case one, the InputStream has been closed already
                    if (in == null) {
                        close = true;
                    }
                } catch (IOException ioe) {
                    // Case two... some unknown error
                    close = true;
                }
                // We need to close this OUTSIDE THE LOOP
                if (close) {
                    toClose.add(i);
                }
            }

            // Second loop... and backwards to preserve index orders
            // as we remove higher values
            limit = toClose.size();
            for (Integer i = limit - 1; i >= 0; i--) {
                // Close
                toTest.get(i).releaseConnection();
                // Then remove
                toTest.remove((int) i);
            }
        }
    }

    /**
     * Build's a GET URL for the requested PID and DSID combination.
     * 
     * @param fedoraPid The fedora PID containing the datastream
     * @param dsId The datastream's ID
     * @return String The URL, possibly null if not possible to encode
     */
    private static String buildGetUrl(String fedoraPid, String dsId) {
        try {
            String returnValue = fedoraGetUrl;
            returnValue += URLEncoder.encode(fedoraPid, "UTF-8");
            if (dsId != null) {
                returnValue += "/";
                returnValue += URLEncoder.encode(dsId, "UTF-8");
            }
            return returnValue;
        } catch (UnsupportedEncodingException ex) {
            log.error("Error encoding URL for object '{}' and DS '{}'", fedoraPid, dsId);
            return null;
        }
    }

    /**
     * A really simple wrapper on closable object to allow trivial close
     * attempts when we are unsure if they are even open.
     * 
     * @param toClose A Closeable Object to try closing
     */
    static void close(Closeable toClose) {
        try {
            toClose.close();
        } catch (IOException ex) {
            // No worries, they may not even be open
        }
    }
}