dk.statsbiblioteket.doms.integration.summa.DOMSReadableStorage.java Source code

Java tutorial

Introduction

Here is the source code for dk.statsbiblioteket.doms.integration.summa.DOMSReadableStorage.java

Source

/*
 * $Id$
 * $Revision$
 * $Date$
 * $Author$
 *
 * The DOMS project.
 * Copyright (C) 2007-2010  The State and University Library
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 dk.statsbiblioteket.doms.integration.summa;

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import dk.statsbiblioteket.doms.client.DomsWSClient;
import dk.statsbiblioteket.doms.client.DomsWSClientImpl;
import dk.statsbiblioteket.doms.client.exceptions.ServerOperationFailed;
import dk.statsbiblioteket.doms.integration.summa.exceptions.DOMSCommunicationError;
import dk.statsbiblioteket.doms.integration.summa.exceptions.RegistryFullException;
import dk.statsbiblioteket.doms.integration.summa.exceptions.UnknownKeyException;
import dk.statsbiblioteket.doms.integration.summa.parsing.ConfigurationKeys;
import dk.statsbiblioteket.doms.integration.summa.registry.SelfCleaningObjectRegistry;
import dk.statsbiblioteket.summa.common.Record;
import dk.statsbiblioteket.summa.common.configuration.Configuration;
import dk.statsbiblioteket.summa.storage.api.QueryOptions;
import dk.statsbiblioteket.summa.storage.api.Storage;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

/**
 * @author Thomas Skou Hansen <tsh@statsbiblioteket.dk>
 */
public class DOMSReadableStorage implements Storage {

    private static final Log log = LogFactory.getLog(DOMSReadableStorage.class);

    /**
     * Three hours in milliseconds. This is the expiration time for the iterator
     * keys returned by this storage.
     */
    private static final long TWELVE_HOURS = 12 * 60 * 60 * 1000;

    /**
     * The delimiter inserted between the Summa base name and the DOMS object
     * UUID when creating the IDs for returned records.
     */
    static final String RECORD_ID_DELIMITER = ":";

    /**
     * By default, allow 100 megabytes per record iterator.
     */
    private static final int DEFAULT_MAX_SIZE_PER_RETRIEVAL = 100 * 1024 * 1024;

    /**
     * The client, connected to the DOMS server specified by the configuration,
     * to retrieve objects from.
     */
    private final DomsWSClient domsClient;

    /**
     * <code>Map</code> containing all the configurations for the Summa base
     * names declared in the configuration passed to the
     * <code>DOMSReadableStorage</code> constructor.
     */
    private final Map<String, BaseDOMSConfiguration> baseConfigurations;

    /**
     * A registry containing all record iterators instantiated by methods
     * returning a key associated with an iterator over their result sets.
     */
    private final SelfCleaningObjectRegistry<SummaRecordIterator> recordIterators;

    /**
     * Create a <code>DOMSReadableStorage</code> instance based on the
     * configuration provided by <code>configuration</code>.
     * 
     *
     * @param configuration
     *            Configuration containing information for mapping Summa base
     *            names to DOMS collections and views.
     * @param domsWSClient The doms client to use.
     * @throws ConfigurationException
     *             if the configuration contains any errors regarding option
     *             values or document structure.
     */
    public DOMSReadableStorage(Configuration configuration, DomsWSClient domsWSClient)
            throws ConfigurationException {

        baseConfigurations = new HashMap<String, BaseDOMSConfiguration>();
        initBaseConfigurations(configuration, baseConfigurations);

        long timeout = configuration.getLong(ConfigurationKeys.ITERATOR_KEY_TIMEOUT, TWELVE_HOURS);
        recordIterators = new SelfCleaningObjectRegistry<SummaRecordIterator>(timeout);

        domsClient = domsWSClient;
        setDomsClientCredentials(configuration);
    }

    /**
     * Create a <code>DOMSReadableStorage</code> instance based on the
     * configuration provided by <code>configuration</code>.
     *
     * Overloaded constructor to support Summa requirement to constructor.
     * Same as calling {@link #DOMSReadableStorage(dk.statsbiblioteket.summa.common.configuration.Configuration, dk.statsbiblioteket.doms.client.DomsWSClient)}
     * except DomsWSClient is hardcoded to {@link DomsWSClientImpl}.
     *
     * @param configuration
     *            Configuration containing information for mapping Summa base
     *            names to DOMS collections and views.
     * @throws ConfigurationException
     *             if the configuration contains any errors regarding option
     *             values or document structure.
     */
    public DOMSReadableStorage(Configuration configuration) throws ConfigurationException {
        this(configuration, new DomsWSClientImpl());
    }

    /**
     * Get the time-stamp for when the latest modification occurred in the DOMS
     * collection view identified by <code>base</code>. This method will resolve
     * <code>base</code> to a DOMS collection and view, using the configuration
     * given to this <code>DOMSReadableStorage</code> instance, and query the
     * DOMS for any changes. Please see the interface documentation for further
     * details.
     * 
     * @param summaBaseID
     *            Summa base ID pointing out the DOMS collection and view to
     *            read from..
     * @return The time-stamp in milliseconds for the latest modification made
     *         in the collection identified by <code>base</code>.
     * 
     * @see dk.statsbiblioteket.summa.storage.api.ReadableStorage#getModificationTime(java.lang.String)
     */
    public long getModificationTime(String summaBaseID) throws IOException {

        if (log.isTraceEnabled()) {
            log.trace("DOMSReadableStorage.getModificationTime(): Called with " + "summaBaseID: " + summaBaseID);
        }

        try {

            // value...
            if (summaBaseID != null) {

                final BaseDOMSConfiguration baseConfiguration = baseConfigurations.get(summaBaseID);

                final URI collectionPID = baseConfiguration.getCollectionPID();
                final String viewID = baseConfiguration.getViewID();
                final String objectState = baseConfiguration.getObjectState();

                return domsClient.getModificationTime(collectionPID.toString(), viewID, objectState);
            } else {

                long mostRecentTimeStamp = 0;
                for (BaseDOMSConfiguration currentConfiguration : baseConfigurations.values()) {

                    final String collectionPIDString = currentConfiguration.getCollectionPID().toString();
                    final String viewID = currentConfiguration.getViewID();
                    final String objectState = currentConfiguration.getObjectState();

                    long currentTimeStamp = domsClient.getModificationTime(collectionPIDString, viewID,
                            objectState);

                    // Update the most recent time-stamp, if necessary.
                    mostRecentTimeStamp = (mostRecentTimeStamp < currentTimeStamp) ? currentTimeStamp
                            : mostRecentTimeStamp;
                }
                if (log.isDebugEnabled()) {
                    log.debug("getModificationTime(): Returning " + "mostRecentTimeStamp: " + mostRecentTimeStamp);
                }
                return mostRecentTimeStamp;
            }
        } catch (ServerOperationFailed serverOperationFailed) {
            final String errorMessage = "Failed retrieving the modification time for base: " + summaBaseID;
            log.warn("getModificationTime(): " + errorMessage, serverOperationFailed);
            throw new IOException(errorMessage, serverOperationFailed);
        }
    }

    public long getRecordsModifiedAfter(long timeStamp, String summaBaseID, QueryOptions options)
            throws IOException {

        if (log.isTraceEnabled()) {
            log.trace("getRecorsModifiedAfter(): Called with timeStamp: " + timeStamp + " summaBaseID: "
                    + summaBaseID + " QueryOptions: " + options);
        }
        // FIXME! Add proper query options handling.

        try {
            Set<String> iteratorBaseIDs = null;
            if (summaBaseID != null) {
                iteratorBaseIDs = new LinkedHashSet<String>();
                iteratorBaseIDs.add(summaBaseID);
            } else {
                iteratorBaseIDs = baseConfigurations.keySet();
            }

            final SummaRecordIterator recordIterator = new SummaRecordIterator(domsClient, baseConfigurations,
                    iteratorBaseIDs, timeStamp, options);

            final long iteratorKey = recordIterators.register(recordIterator);

            if (log.isDebugEnabled()) {
                log.debug("getRecordsModifedAfter(): Returning iteratorKey = " + iteratorKey);
            }
            return iteratorKey;
        } catch (RegistryFullException registryFullException) {
            final String errorMessage = "Failed creating an iterator for " + "retrieval of records from base "
                    + "(base=" + summaBaseID + ") modified after time-stamp (timeStamp=" + timeStamp
                    + "), using QueryOptions: " + options;
            log.warn("getRecordsModifedAfter(long, String, QueryOptions): " + errorMessage, registryFullException);
            throw new IOException(errorMessage, registryFullException);
        }
    }

    public List<Record> next(long iteratorKey, int maxRecords)
            throws IOException, IllegalArgumentException, NoSuchElementException {

        if (log.isTraceEnabled()) {
            log.trace("next(long, int): Called with iteratorKey = " + iteratorKey + " maxRecords = " + maxRecords);
        }

        try {
            final SummaRecordIterator recordIterator = recordIterators.get(iteratorKey);

            if (recordIterator.hasNext() == false) {
                // The iterator has reached the end and thus it is obsolete. Let
                // the wolves have it...
                recordIterators.remove(iteratorKey);

                final String errorMessage = "The iterator is out of records " + "(iterator key = " + iteratorKey
                        + ")";

                log.warn("next(long, int): " + errorMessage);
                throw new NoSuchElementException(errorMessage);
            }

            final List<Record> resultList = new LinkedList<Record>();
            int recordCounter = 0;
            int size = 0;
            while (recordIterator.hasNext() && recordCounter < maxRecords
                    && size < baseConfigurations
                            .get(recordIterator.getCurrentBaseRecordDescription().getSummaBaseID())
                            .getMaxSizePerRetrieval()) {
                Record record = recordIterator.next();
                resultList.add(record);
                recordCounter++;
                size += record.getContent().length;
                if (log.isTraceEnabled()) {
                    log.trace("Size: " + size + ", maxSize: "
                            + baseConfigurations
                                    .get(recordIterator.getCurrentBaseRecordDescription().getSummaBaseID())
                                    .getMaxSizePerRetrieval());
                }

            }

            if (log.isDebugEnabled()) {
                log.debug("next(long, int): Returning " + resultList.size() + " records.");
            }
            return resultList;
        } catch (UnknownKeyException unknownKeyException) {

            // The iterator key is unknown to the registry. It may be because it
            // has expired.
            final String errorMessage = "Unknown iterator (iterator key = " + iteratorKey
                    + "). Failed retrieving up to " + maxRecords + " records. Has the key expired?";
            log.warn("next(long, int): " + errorMessage);

            throw new IllegalArgumentException(errorMessage, unknownKeyException);
        } catch (DOMSCommunicationError domsCommunicationError) {

            // Translate communication/server errors to IOExceptions!
            final String errorMessage = "next() operation on this iterator " + "(iterator key = " + iteratorKey
                    + ") failed due to a " + "server or communication error. Failed retrieving up to " + maxRecords
                    + " records.";

            log.warn("next(long, int): " + errorMessage);
            throw new IOException(errorMessage, domsCommunicationError);
        }
    }

    public Record next(long iteratorKey) throws IOException, IllegalArgumentException, NoSuchElementException {

        if (log.isTraceEnabled()) {
            log.trace("next(long): Called with iteratorKey = " + iteratorKey);
        }

        try {
            final Iterator<Record> recordIterator = recordIterators.get(iteratorKey);

            log.debug("next(long): Returning next element for iterator key: " + iteratorKey);
            return recordIterator.next();
        } catch (NoSuchElementException noSuchElementException) {
            // The iterator has reached the end and thus it is obsolete. Let the
            // wolves have it...
            try {
                recordIterators.remove(iteratorKey);
            } catch (UnknownKeyException unknownKeyException) {
                log.error("next(long): Failed removing iterator from the " + "registry. This is not possible!",
                        unknownKeyException);
                // Just continue although this is probably due to an error
                // somewhere, as the storage will not break because of this.
            }

            if (log.isDebugEnabled()) {
                log.debug("next(long): The iterator is out of records (" + "iterator key = " + iteratorKey + ")");
            }

            // Re-throw.
            throw noSuchElementException;
        } catch (UnknownKeyException unknownKeyException) {

            // The iterator key is unknown to the registry. It may be because it
            // has expired.

            final String errorMessage = "Unknown iterator (iterator key = " + iteratorKey
                    + "). Has the key expired?";
            log.warn("next(long): " + errorMessage);

            throw new IllegalArgumentException(errorMessage, unknownKeyException);
        } catch (DOMSCommunicationError domsCommunicationError) {

            // Translate communication/server errors to IOExceptions!
            final String errorMessage = "next() operation on this iterator " + "(iterator key = " + iteratorKey
                    + ") failed due to a " + "server or communication error.";

            log.warn("next(long): " + errorMessage, domsCommunicationError);
            throw new IOException(errorMessage, domsCommunicationError);
        }
    }

    public Record getRecord(String summaRecordID, QueryOptions options)
            throws IOException, IllegalArgumentException {

        log.trace("getRecord(String , QueryOptions): Called with " + "summaRecordID: " + summaRecordID
                + " QueryOptions: " + options);

        // FIXME! Add proper query options handling.

        try {
            // All records previously returned by the DOMS storage have had the
            // base name prepended to the DOMS entry object PID. Thus, we know
            // that there is a base name and a RECPRD_ID_DELIMITER in the
            // beginning of the ID.
            final int baseDelimiterPosition = summaRecordID.indexOf(RECORD_ID_DELIMITER);

            final String base = summaRecordID.substring(0, baseDelimiterPosition);

            final String entryObjectPID = summaRecordID.substring(baseDelimiterPosition + 1);

            final BaseDOMSConfiguration baseConfiguration = baseConfigurations.get(base);

            if (baseConfiguration == null) {

                final String errorMessage = "Unknown Summa base ID: " + base
                        + " in the ID of the requested record: " + summaRecordID;
                log.warn("getRecord(String): " + errorMessage);

                throw new IllegalArgumentException(errorMessage);
            }
            final String viewID = baseConfiguration.getViewID();
            final String viewBundle = domsClient.getViewBundle(entryObjectPID, viewID);

            final Record resultRecord = new Record(summaRecordID, base, viewBundle.getBytes());

            if (log.isDebugEnabled()) {
                log.debug("getRecord(String , QueryOptions): Returning: " + resultRecord);
            }

            return resultRecord;

        } catch (ServerOperationFailed serverOperationFailed) {
            final String errorMessage = "Failed retrieving record (record id = '" + summaRecordID
                    + "', using the query options: " + options;

            log.warn("getRecord(String , QueryOptions): " + errorMessage, serverOperationFailed);
            throw new IOException(errorMessage, serverOperationFailed);
        }
    }

    public List<Record> getRecords(List<String> summaRecordIDs, QueryOptions options)
            throws IOException, IllegalArgumentException {

        if (log.isTraceEnabled()) {
            log.trace("getRecords(List<String>, QueryOptions): called with " + "summaRecordsIDs: " + summaRecordIDs
                    + " QueryOptions: " + options);
        }

        List<Record> resultList = new LinkedList<Record>();
        for (String recordID : summaRecordIDs) {
            resultList.add(getRecord(recordID, options));
        }

        if (log.isDebugEnabled()) {
            log.debug("getRecords(List<String>, QueryOptions): Returning with " + "resultList: " + resultList);
        }
        return resultList;
    }

    public void flush(Record record, QueryOptions options) throws IOException {
        log.warn("flush(Record, QueryOptions): Un-implemented method called " + "with record: " + record
                + " options: " + options);
        throw new NotImplementedException();
    }

    public void flush(Record record) throws IOException {
        log.warn("flush(Record) Un-implemented method called with record: " + record);
        throw new NotImplementedException();
    }

    public void flushAll(List<Record> records, QueryOptions options) throws IOException {
        log.warn("flushAll(List, QueryOptions) un-implemented method called " + "with records: " + records
                + " options: " + options);
        throw new NotImplementedException();
    }

    public void flushAll(List<Record> records) throws IOException {
        log.warn("flushAll(List): Un-implemented method called with records: " + records);
        throw new NotImplementedException();
    }

    public void close() throws IOException {
        log.warn("close(): Un-implemented method called.");
        throw new NotImplementedException();
    }

    public void clearBase(String base) throws IOException {
        log.warn("clearBase(String): Un-implemented method called with base: " + base);
        throw new NotImplementedException();
    }

    public String batchJob(String jobName, String base, long minMtime, long maxMtime, QueryOptions options)
            throws IOException {
        log.warn("batchJob(String, String, long, long, "
                + "QueryOptions) Un-implemented method called with jobName: " + jobName + " base: " + base
                + " minMtime: " + minMtime + " maxMtime:" + maxMtime + " options: " + options);
        throw new NotImplementedException();
    }

    /**
     * Create a <code>DOMSReadableStorage</code> instance which is logged into
     * the DOMS server specified in <code>configuration</code> using the
     * username, password and web service end-point also specified by
     * <code>configuration</code>.
     * 
     *
     *
     * @param configuration
     *            a <code>DOMSReadableStorage</code> configuration object.
     * @return a reference to the <code>DOMSReadableStorage</code> instance.
     * @throws ConfigurationException
     *             if the configuration contains any illegal values or
     *             structures.
     */
    protected void setDomsClientCredentials(Configuration configuration) throws ConfigurationException {

        if (log.isTraceEnabled()) {
            log.trace("setDomsClientCredentials(Configuration): Called with configuration: " + configuration);
        }

        final String userName = configuration.getString(ConfigurationKeys.DOMS_USER_NAME);

        final String password = configuration.getString(ConfigurationKeys.DOMS_PASSWORD);

        if (userName == null || password == null) {

            final String errorMessage = "Invalid DOMS user credentials in the" + " configuration. username = '"
                    + userName + "'  password = '" + password + "'";
            log.warn("setDomsClientCredentials(Configuration): " + errorMessage);

            throw new ConfigurationException(errorMessage);
        }

        final String domsWSEndpointURL = configuration.getString(ConfigurationKeys.DOMS_API_WEBSERVICE_URL);
        try {
            final URL domsWSAPIEndpoint = new URL(domsWSEndpointURL);
            domsClient.setCredentials(domsWSAPIEndpoint, userName, password);

            if (log.isDebugEnabled()) {
                log.debug("setDomsClientCredentials(Configuration): returning a DOMSWSClient "
                        + "instance logged in with user = '" + userName + " and password = '" + password + "' on "
                        + domsWSEndpointURL);
            }
        } catch (MalformedURLException malformedURLException) {

            final String errorMessage = "Failed connecting to the DOMS API " + "webservice with the URL" + " ("
                    + domsWSEndpointURL + ") specified in the configuration.";

            log.warn("setDomsClientCredentials(Configuration): " + errorMessage);
            throw new ConfigurationException(errorMessage, malformedURLException);
        }
    }

    /**
     * Initialise the provided map of base configurations with the relations
     * between the Summa base names and views of DOMS collections as described
     * by the configuration provided by the <code>configuration</code>
     * parameter.
     * <p/>
     * This method will add an entry to the <code>baseConfigurationsMap</code>
     * for each Summa base configuration found in the configuration document.
     * Each of these configurations will be mapped to a base name.
     * 
     * @param configuration
     *            Configuration document describing relations between Summa base
     *            names and DOMS collections, content models and views.
     * 
     * @param baseConfigurationsMap
     *            The map to initialise.
     * @throws ConfigurationException
     *             if the configuration contains any illegal values or document
     *             structures.
     */
    private void initBaseConfigurations(Configuration configuration,
            Map<String, BaseDOMSConfiguration> baseConfigurationsMap) throws ConfigurationException {

        if (log.isTraceEnabled()) {
            log.trace("initBaseConfigurations(Configuration, Map<String, "
                    + "BaseDOMSConfiguration>): Called with configuration: " + configuration
                    + " baseConfigurationsMap:" + baseConfigurationsMap);
        }

        String baseID = null;
        BaseDOMSConfiguration previousConfig = null;
        try {
            final List<Configuration> baseConfigurations = configuration
                    .getSubConfigurations(ConfigurationKeys.ACCESSIBLE_COLLECTION_BASES);

            for (Configuration subConfiguration : baseConfigurations) {
                baseID = subConfiguration.getString(ConfigurationKeys.COLLECTION_BASE_ID);

                final String collectionURI = subConfiguration.getString(ConfigurationKeys.COLLECTION_PID);

                final URI collectionPID = new URI(collectionURI);

                final String viewID = subConfiguration.getString(ConfigurationKeys.VIEW_ID);

                final String objectState = subConfiguration.getString(ConfigurationKeys.OBJECT_STATE);

                final long recordCountPerRetrieval = subConfiguration
                        .getLong(ConfigurationKeys.OBJECT_COUNT_PER_RETRIEVAL, 10000);

                final long maxSizePerRetrieval = subConfiguration.getLong(ConfigurationKeys.MAX_SIZE_PER_RETRIEVAL,
                        DEFAULT_MAX_SIZE_PER_RETRIEVAL);

                BaseDOMSConfiguration newBaseConfig = new BaseDOMSConfiguration(collectionPID, viewID, objectState,
                        recordCountPerRetrieval, maxSizePerRetrieval);

                previousConfig = baseConfigurationsMap.put(baseID, newBaseConfig);
                if (previousConfig != null) {
                    // We want to throw an exception here, however, there is no
                    // need to get it nested in a fault barrier exception. Thus,
                    // break and throw later.
                    break;
                }
            }

            if (log.isTraceEnabled()) {
                log.trace("initBaseConfigurations(Configuration, Map<String, " + "BaseDOMSConfiguration>): "
                        + "Successfully added " + baseConfigurations.size() + " base configurations to"
                        + " the internal map. Returning.");
            }

        } catch (Exception exception) {

            final String errorMessage = "Could not retrieve the collection base (base ID = '" + baseID
                    + "' configuration information.";

            log.warn("initBaseConfigurations(Configuration, Map<String, " + "BaseDOMSConfiguration>): "
                    + errorMessage);

            throw new ConfigurationException(errorMessage, exception);
        }
        if (previousConfig != null) {
            final String errorMessage = "base (base ID = '" + baseID + "' has already been associated with a"
                    + " DOMS view.";

            log.warn("initBaseConfigurations(Configuration, Map<String, " + "BaseDOMSConfiguration>):"
                    + errorMessage);

            throw new ConfigurationException(errorMessage);
        }
    }

}