org.lockss.config.Tdb Maven / Gradle / Ivy
Show all versions of lockss-core Show documentation
/*
* $Id$
*/
/*
Copyright (c) 2000-2012 Board of Trustees of Leland Stanford Jr. University,
all rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
STANFORD UNIVERSITY BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Except as contained in this notice, the name of Stanford University shall not
be used in advertising or otherwise to promote the sale, use or other dealings
in this Software without prior written authorization from Stanford University.
*/
package org.lockss.config;
import java.io.*;
import java.util.*;
import org.apache.commons.collections4 .*;
import org.apache.commons.collections4.iterators .*;
import org.lockss.util.*;
/**
* This class represents a title database (TDB). The TDB consists of
* hierarchy of TdbPublisher
s, and TdbAu
s.
* Special indexing provides fast access to all TdbAu
s for
* a specified plugin ID.
*
* @author Philip Gust
* @version $Id$
*/
import com.jcabi.aspects.*;
@Loggable(value = Loggable.TRACE, prepend = true)
public class Tdb {
/**
* Set up logger
*/
protected final static Logger logger = Logger.getLogger();
/**
* A map of AUs per plugin, for this configuration
* (provides faster access for Plugins)
*/
private final Map> pluginIdTdbAuIdsMap =
new HashMap>(4, 1F);
/**
* Map of publisher names to TdBPublishers for this configuration
*/
private final Map tdbPublisherMap =
new HashMap(4, 1F);
/**
* Map of provider names to TdBProviders for this configuration
*/
private final Map tdbProviderMap =
new HashMap(4, 1F);
/**
* Determines whether more AUs can be added.
*/
private boolean isSealed = false;
/**
* The total number of TdbAus in this TDB (sum of collections in pluginIdTdbAus map
*/
private int tdbAuCount = 0;
/**
* Prefix appended to generated unknown title
*/
private static final String UNKNOWN_TITLE_PREFIX = "Title of ";
/**
* Prefix appended to generated unknown publisher
*/
static final String UNKNOWN_PUBLISHER_PREFIX = "Publisher of ";
/**
* Prefix appended to generated unknown publisher
*/
static final String UNKNOWN_PROVIDER_PREFIX = "Provider of ";
/**
* This exception is thrown by Tdb related classes in place of an
* unchecked IllegalStateException when an operation cannot be
* performed because it is incompatible with state of the Tdb.
*
* This class inherits from IOException to avoid having higher
* level routines that already have to handle IOException when
* creating and copying Configuration objects from having to
* also handle this exception.
*
* @author Philip Gust
* @version $Id$
*/
@SuppressWarnings("serial")
static public class TdbException extends Exception {
/**
* Constructs a new exception with the specified detail message. The
* cause is not initialized, and may subsequently be initialized by
* a call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public TdbException(String message) {
super(message);
}
/**
* Constructs a new exception with the specified detail message and
* cause.
Note that the detail message associated with
* cause
is not automatically incorporated in
* this exception's detail message.
*
* @param message the detail message (which is saved for later retrieval
* by the {@link #getMessage()} method).
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A null value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public TdbException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new exception with the specified cause and a detail
* message of (cause==null ? null : cause.toString()) (which
* typically contains the class and detail message of cause).
* This constructor is useful for exceptions that are little more than
* wrappers for other throwables (for example, {@link
* java.security.PrivilegedActionException}).
*
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A null value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public TdbException(Throwable cause) {
super(cause);
}
}
/**
* Register the au with this Tdb for its plugin.
*
* @param au the TdbAu
* @return false
if already registered,
* otherwise true
* @throws TdbException if adding new TdbAu with duplicate id
*/
private boolean addTdbAuForPlugin(TdbAu au) throws TdbException {
// add AU to list for plugins
String pluginId = au.getPluginId();
Map auids = pluginIdTdbAuIdsMap.get(pluginId);
if (auids == null) {
auids = new HashMap(4, 1F);
pluginIdTdbAuIdsMap.put(pluginId, auids);
}
TdbAu otherAu = auids.put(au.getId(),au);
if (otherAu != null) {
// check existing entry
if (au == otherAu) {
// OK if same AU
return false;
}
// restore otherAu and throw exception
auids.put(otherAu.getId(),otherAu);
// throw exception if adding another AU with duplicate au id.
throw new TdbException("New au with duplicate id: " + au.getName());
}
// increment the total AU count;
tdbAuCount++;
return true;
}
/**
* Unregister the au with this Tdb for its plugin.
*
* @param au the TdbAu
* @return false
if au was not registered, otherwise true
*/
private boolean removeTdbAuForPlugin(TdbAu au) {
// if can't add au to title, we need to undo the au
// registration and re-throw the exception we just caught
String pluginId = au.getPluginId();
Map c = pluginIdTdbAuIdsMap.get(pluginId);
if (c.remove(au.getId()) != null) {
if (c.isEmpty()) {
pluginIdTdbAuIdsMap.remove(c);
}
tdbAuCount--;
return true;
}
return false;
}
/**
* Add TdbProvider to Tdb
* @param provider the TdbProvider
* @return true
if a new provider was added, false
* if this providerr is already added
* @throws TdbException if trying to add new provider with duplicate name
*/
public boolean addTdbProvider(TdbProvider provider) throws TdbException {
String providerName = provider.getName();
TdbProvider oldProvider = tdbProviderMap.put(providerName, provider);
if ((oldProvider != null) && (oldProvider != provider)) {
// restore old publisher and report error
tdbProviderMap.put(providerName, oldProvider);
throw new TdbException("New au provider with duplicate name: "
+ providerName);
}
return (oldProvider == null);
}
/**
* Add TdbPublisher to Tdb
* @param publisher the TdbPublisher
* @return true
if a new publisher was added, false
* if this publisher is already added
* @throws TdbException if trying to add new publisher with duplicate name
*/
public boolean addTdbPublisher(TdbPublisher publisher) throws TdbException {
String pubName = publisher.getName();
TdbPublisher oldPublisher = tdbPublisherMap.put(pubName, publisher);
if ((oldPublisher != null) && (oldPublisher != publisher)) {
// restore old publisher and report error
tdbPublisherMap.put(pubName, oldPublisher);
throw new TdbException("New au publisher with duplicate name: "
+ pubName);
}
return (oldPublisher == null);
}
/**
* Add a new TdbAu to this title database. The TdbAu must have
* its pluginID, provider, and title set. The TdbAu's title must also
* have its titleId and publisher set. The publisher name must be unique
* to all publishers in this Tdb.
*
* @param au the TdbAu to add.
* @return true
if new AU was added, false
if
* this AU was previously added
* @throws TdbException if Tdb is sealed, this is a duplicate au, or
* the au's publisher or provider is a duplicate
*/
public boolean addTdbAu(TdbAu au) throws TdbException {
if (au == null) {
throw new IllegalArgumentException("TdbAu cannot be null");
}
// verify not sealed
if (isSealed()) {
throw new TdbException("Cannot add TdbAu to sealed Tdb");
}
// validate title
TdbTitle title = au.getTdbTitle();
if (title == null) {
throw new IllegalArgumentException("TdbAu's title not set");
}
// validate publisher
TdbPublisher publisher = title.getTdbPublisher();
if (publisher == null) {
throw new IllegalArgumentException("TdbAu's publisher not set");
}
// validate provider
TdbProvider provider = au.getTdbProvider();
if (provider == null) {
String pubName = publisher.getName();
// assume the provider is the same as the publisher
provider = tdbProviderMap.get(pubName);
if (provider == null) {
provider = new TdbProvider(pubName);
}
// add publisher provider to the au
provider.addTdbAu(au);
if (logger.isDebug3()) {
logger.debug3("Creating default provider for publisher " + pubName);
}
}
boolean newPublisher = false;
boolean newProvider = false;
boolean newAu = false;
try {
// add au publisher if not already added
newPublisher = addTdbPublisher(publisher);
// add au provider if not already added
newProvider = addTdbProvider(provider);
// add au if not already added
newAu = addTdbAuForPlugin(au);
} catch (TdbException ex) {
// remove new publisher and new provider, and report error
if (newPublisher) {
tdbPublisherMap.remove(publisher.getName());
}
if (newProvider) {
tdbProviderMap.remove(provider.getName());
}
TdbException ex2 = new TdbException("Cannot register au " + au.getName());
ex2.initCause(ex);
throw ex2;
}
return newAu;
}
/**
* Seals a Tdb against further additions.
*/
public void seal() {
isSealed = true;
}
/**
* Determines whether this Tdb is sealed.
*
* @return true
if sealed
*/
public boolean isSealed() {
return isSealed;
}
/**
* Determines whether the title database is empty.
*
* @return true
if the title database has no entries
*/
public boolean isEmpty() {
return pluginIdTdbAuIdsMap.isEmpty();
}
/**
* Return an object describing the differences between the Tdbs.
* Logically a symmetric operation but currently records only changes and
* addition, not deletions.
* @param newTdb the new Tdb
* @param oldTdb the previous Tdb
* @return a {@link Tdb.Differences}
*/
public static Differences computeDifferences(Tdb newTdb, Tdb oldTdb) {
if (newTdb == null) {
newTdb = new Tdb();
}
return newTdb.computeDifferences(oldTdb);
}
Differences computeDifferences(Tdb oldTdb) {
if (oldTdb == null) {
return new AllDifferences(this);
} else {
return new Differences(this, oldTdb);
}
}
/**
* Adds to the {@link Tdb.Differences} the differences found between
* oldTdb and this Tdb
*
* @param the {@link Tdb.Differences} to which to add items.
* @param otherTdb a Tdb
*/
private void addDifferences(Differences diffs, Tdb oldTdb) {
// process publishers
Map oldPublishers = oldTdb.getAllTdbPublishers();
for (TdbPublisher oldPublisher : oldPublishers.values()) {
if (!this.tdbPublisherMap.containsKey(oldPublisher.getName())) {
// add pluginIds for publishers in tdb that are not in this Tdb
diffs.addPublisher(oldPublisher, Differences.Type.Old);
}
}
for (TdbPublisher thisPublisher : tdbPublisherMap.values()) {
TdbPublisher oldPublisher = oldPublishers.get(thisPublisher.getName());
if (oldPublisher == null) {
// add pluginIds for publisher in this Tdb that is not in tdb
diffs.addPublisher(thisPublisher, Differences.Type.New);
} else {
// add pluginIds for publishers in both Tdbs that are different
thisPublisher.addDifferences(diffs, oldPublisher);
}
}
// process providers
Map oldProviders = oldTdb.getAllTdbProviders();
for (TdbProvider oldProvider : oldProviders.values()) {
if (!this.tdbProviderMap.containsKey(oldProvider.getName())) {
// add pluginIds for providers in tdb that are not in this Tdb
diffs.addProvider(oldProvider, Differences.Type.Old);
}
}
for (TdbProvider thisProvider : tdbProviderMap.values()) {
if (!oldTdb.tdbProviderMap.containsKey(thisProvider.getName())) {
// add pluginIds for provider in this Tdb that is not in tdb
diffs.addProvider(thisProvider, Differences.Type.New);
}
}
}
/**
* Determines two Tdbs are equal. Equality is based on having
* equal TdbPublishers, and their child TdbTitles and TdbAus.
*
* @param o the other object
* @return true
iff they are equal Tdbs
*/
public boolean equals(Object o) {
// check for identity
if (this == o) {
return true;
}
if (o instanceof Tdb) {
try {
// if no exception thrown, there are no differences
// because the method did not try to modify the set
Differences diffs = new Differences.Unmodifiable();
addDifferences(diffs, (Tdb)o);
return true;
} catch (UnsupportedOperationException ex) {
// differences because method tried to add to unmodifiable set
} catch (IllegalArgumentException ex) {
// if something was wrong with the other Tdb
} catch (IllegalStateException ex) {
// if something is wrong with this Tdb
}
}
return false;
}
/**
* Not supported for this class.
*
* @throws UnsupportedOperationException
*/
public int hashCode() {
throw new UnsupportedOperationException();
}
/**
* Merge other Tdb into this one. Makes copies of otherTdb's non-duplicate
* TdbPublisher, TdbTitle, and TdbAu objects and their non-duplicate children.
* The object themselves are not merged.
*
* @param otherTdb the other Tdb
* @throws TdbException if Tdb is sealed
*/
public void copyFrom(Tdb otherTdb) throws TdbException {
// ignore inappropriate Tdb values
if ((otherTdb == null) || (otherTdb == this)) {
return;
}
if (isSealed()) {
throw new TdbException("Cannot add otherTdb AUs to sealed Tdb");
}
// merge non-duplicate publishers of otherTdb
boolean tdbIsNew = tdbPublisherMap.isEmpty();
for (TdbPublisher otherPublisher : otherTdb.getAllTdbPublishers().values()) {
String pubName = otherPublisher.getName();
TdbPublisher thisPublisher;
boolean publisherIsNew = true;
if (tdbIsNew) {
// no need to check for existing publisher if TDB is new
thisPublisher = new TdbPublisher(pubName);
tdbPublisherMap.put(pubName, thisPublisher);
} else {
thisPublisher = tdbPublisherMap.get(pubName);
publisherIsNew = (thisPublisher == null);
if (publisherIsNew) {
// copy publisher if not present in this Tdb
thisPublisher = new TdbPublisher(pubName);
tdbPublisherMap.put(pubName, thisPublisher);
}
}
// merge non-duplicate titles of otherPublisher into thisPublisher
for (TdbTitle otherTitle : otherPublisher.getTdbTitles()) {
String titleName = otherTitle.getName();
String otherId = otherTitle.getId();
TdbTitle thisTitle;
boolean titleIsNew = true;
if (publisherIsNew) {
// no need to check for existing title if publisher is new
thisTitle = otherTitle.copyForTdbPublisher(thisPublisher);
thisPublisher.addTdbTitle(thisTitle);
} else {
thisTitle = thisPublisher.getTdbTitleById(otherId);
titleIsNew = (thisTitle == null);
if (titleIsNew) {
// copy title if not present in this publisher
thisTitle = otherTitle.copyForTdbPublisher(thisPublisher);
thisPublisher.addTdbTitle(thisTitle);
} else if (! thisTitle.getName().equals(otherTitle.getName())) {
// error because it could lead to a missing title -- one probably has a typo
// (what about checking other title elements too?)
logger.error("Ignorning duplicate title entry: \"" + titleName
+ "\" with the same ID as \""
+ thisTitle.getName() + "\"");
}
}
// merge non-duplicate TdbAus of otherTitle into thisTitle
for (TdbAu otherAu : otherTitle.getTdbAus()) {
String auName = otherAu.getName();
String providerName = otherAu.getProviderName();
TdbAu thisAu = getTdbAuById(otherAu);
if (thisAu == null) {
// copy new TdbAu
thisAu = otherAu.copyForTdbTitle(thisTitle);
// add copy to provider of same name
TdbProvider thisProvider = getTdbProvider(providerName);
if (thisProvider == null) {
// add new provider to this Tdb
thisProvider = new TdbProvider(providerName);
tdbProviderMap.put(providerName, thisProvider);
}
thisProvider.addTdbAu(thisAu);
// finish adding this AU
addTdbAuForPlugin(thisAu);
} else {
// ignore and log error if existing AU is not identical
if ( !thisAu.getName().equals(auName)
|| !thisAu.getTdbTitle().getName().equals(titleName)
|| !thisAu.getTdbPublisher().getName().equals(pubName)
|| !thisAu.getTdbProvider().getName().equals(providerName)) {
logger.error("Ignoring duplicate au entry id \"" + thisAu.getId()
+ " for provider \"" + providerName
+ "\" (" + thisAu.getProviderName() + "), publisher \""
+ pubName + "\" (" + thisAu.getPublisherName()
+ "), title \"" + titleName + "\" ("
+ thisAu.getPublicationTitle() + "), name \""
+ auName + "\" (" + thisAu.getName() + ")");
}
}
}
}
}
}
/**
* Get existing TdbAu with same Id as another one.
* @param otherAu another TdbAu
* @return an existing TdbAu already in thisTdb
*/
public TdbAu getTdbAuById(TdbAu otherAu) {
Map map = pluginIdTdbAuIdsMap.get(otherAu.getPluginId());
return (map == null) ? null : map.get(otherAu.getId());
}
/**
* Get existing TdbAu with the the specified id.
* @param auId the TdbAu.Id
* @return the existing TdbAu or null
if not in this Tdb
*/
public TdbAu getTdbAuById(TdbAu.Id auId) {
return getTdbAuById(auId.getTdbAu());
}
/**
* Returns a collection of TdbAus for the specified plugin ID.
*
* Note: the returned collection should not be modified.
*
* @param pluginId the plugin ID
* @return a collection of TdbAus for the plugin; null
* if no TdbAus for the specified plugin in this configuration.
*/
public Collection getTdbAuIds(String pluginId) {
Map auIdMap = pluginIdTdbAuIdsMap.get(pluginId);
return (auIdMap != null) ?
auIdMap.keySet() : Collections.emptyList();
}
/**
* Returns the set of plugin ids for this Tdb.
* @return the set of all plugin ids for this Tdb.
*/
public Set getAllPluginsIds() {
return (pluginIdTdbAuIdsMap != null)
? Collections.unmodifiableSet(pluginIdTdbAuIdsMap.keySet())
: Collections.emptySet();
}
/**
* Get a list of all the TdbAu.Ids for this Tdb
*
* @return a collection of TdbAu objects
*/
public Set getAllTdbAuIds() {
if (pluginIdTdbAuIdsMap == null) {
return Collections. emptySet();
}
Set allAuIds = new HashSet();
// For each plugin's AU set, add them all to the set.
for (Map auIdMap : pluginIdTdbAuIdsMap.values()) {
allAuIds.addAll(auIdMap.keySet());
}
return allAuIds;
}
/**
* Return the number of TdbAus in this Tdb.
*
* @return the total TdbAu count
*/
public int getTdbAuCount() {
return tdbAuCount;
}
/**
* Return the number of TdbTitles in this Tdb.
*
* @return the total TdbTitle count
*/
public int getTdbTitleCount() {
int titleCount = 0;
for (TdbPublisher publisher : tdbPublisherMap.values()) {
titleCount += publisher.getTdbTitleCount();
}
return titleCount;
}
/**
* Return the number of TdbPublishers in this Tdb.
*
* @return the total TdbPublisher count
*/
public int getTdbPublisherCount() {
return tdbPublisherMap.size();
}
/**
* Return the number of TdbProviders in this Tdb.
*
* @return the total TdbProvider count
*/
public int getTdbProviderCount() {
return tdbProviderMap.size();
}
/**
* Add a new TdbAu from properties. This method recognizes
* properties of the following form:
*
* Properties p = new Properties();
* p.setProperty("title", "Air & Space Volume 1)");
* p.setProperty("journalTitle", "Air and Space");
* p.setProperty("plugin", "org.lockss.plugin.smithsonian.SmithsonianPlugin");
* p.setProperty("pluginVersion", "4");
* p.setProperty("issn", "0886-2257");
* p.setProperty("param.1.key", "volume");
* p.setProperty("param.1.value", "1");
* p.setProperty("param.2.key", "year");
* p.setProperty("param.2.value", "2001");
* p.setProperty("param.2.editable", "true");
* p.setProperty("param.3.key", "journal_id");
* p.setProperty("param.3.value", "0886-2257");
* p.setProperty("attributes.publisher", "Smithsonian Institution");
*
*
* The "attributes.publisher" property is used to identify the publisher.
* If a unique journalID is specified it is used to select among titles
* for a publisher. A journalID can be specified indirectly through a
* "journal_id" param or an "issn" property. If a journalId is not
* specified, the "journalTitle" property is used to select the the title.
*
* Properties other than "param", "attributes", "title", "journalTitle",
* "journalId", and "plugin" are converted to attributes of the AU. Only
* "title" and "plugin" are required properties. If "attributes.publisher"
* or "journalTitle" are missing, their values are synthesized from the
* "title" property.
*
* @param props a map of title properties
* @return the TdbAu that was added
* @throws TdbException if this Tdb is sealed, or the
* AU already exists in this Tdb
*/
public TdbAu addTdbAuFromProperties(Properties props) throws TdbException {
if (props == null) {
throw new IllegalArgumentException("properties cannot be null");
}
// verify not sealed
if (isSealed()) {
throw new TdbException("cannot add au to sealed TDB");
}
// generate new TdbAu from properties
TdbAu au = newTdbAu(props);
// add au for plugin assuming it is not a duplicate
try {
addTdbAuForPlugin(au);
} catch (TdbException ex) {
// au already registered -- report existing au
TdbAu existingAu = getTdbAuById(au);
String titleName = getTdbTitleName(props, au);
if (!titleName.equals(existingAu.getTdbTitle().getName())) {
throw new TdbException(
"Cannot add duplicate au entry: \"" + au.getName()
+ "\" for title \"" + titleName
+ "\" with same definition as existing au entry: \""
+ existingAu.getName() + "\" for title \""
+ existingAu.getTdbTitle().getName() + "\" to title database");
} else if (!existingAu.getName().equals(au.getName())) {
// error because it could lead to a missing AU -- one probably has a typo
throw new TdbException(
"Cannot add duplicate au entry: \"" + au.getName()
+ "\" with the same definition as \"" + existingAu.getName()
+ "\" for title \"" + titleName + "\" to title database");
} else {
throw new TdbException(
"Cannot add duplicate au entry: \"" + au.getName()
+ "\" for title \"" + titleName + "\" to title database");
}
}
// get or create the TdbTitle for this
TdbTitle title = getTdbTitle(props, au);
try {
// add AU to title
title.addTdbAu(au);
} catch (TdbException ex) {
// if we can't add au to title, remove for plugin and re-throw exception
removeTdbAuForPlugin(au);
throw ex;
}
// get or create the TdbProvider for this
TdbProvider provider = getTdbProvider(props, au);
try {
// add AU to title
provider.addTdbAu(au);
} catch (TdbException ex) {
// if we can't add au to provider, remove for plugin and title,
// and re-throw exception
removeTdbAuForPlugin(au);
// TODO: what to do about unregistering with the tdbTitle?
throw ex;
}
// process title links
Map> linkMap = new HashMap>();
for (Map.Entry