/*
* Copyright 2007, 2008 (C) Tom Parker <thpr@users.sourceforge.net>
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library 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 Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package pcgen.cdom.reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import javax.swing.event.EventListenerList;
import pcgen.base.lang.CaseInsensitiveString;
import pcgen.base.util.FixedStringList;
import pcgen.base.util.FormatManager;
import pcgen.base.util.HashMapToInstanceList;
import pcgen.base.util.Indirect;
import pcgen.base.util.KeyMap;
import pcgen.cdom.base.CDOMObject;
import pcgen.cdom.base.CDOMReference;
import pcgen.cdom.base.Loadable;
import pcgen.cdom.content.RollMethod;
import pcgen.util.Logging;
import pcgen.util.StringPClassUtil;
/**
* An AbstractReferenceManufacturer is a concrete, but abstract object capable
* of creating CDOMReferences of a given "form". That "form" includes a specific
* Class of Loadable, or a specific Class/Category for Categorized Loadable
* (this class does not make distinction between the Class and Class/Categorized
* cases)
*
* The Class is designed to share significant common code between
* implementations of the ReferenceManufacturer interface.
*
* @param <T>
* The Class of object this AbstractReferenceManufacturer can
* manufacture
*/
public abstract class AbstractReferenceManufacturer<T extends Loadable>
implements ReferenceManufacturer<T>
{
private boolean isResolved = false;
private final ManufacturableFactory<T> factory;
/**
* The "ALL" reference, if it is ever referenced. This ensures that only one
* "ALL" reference is ever built (and allows it to be reused if the
* reference is requested a second time). This also stores the reference so
* that it can be appropriately resolved when resolveReferences() is called.
*/
private CDOMGroupRef<T> allRef;
/**
* Storage for "TYPE" references. This ensures that only one "TYPE"
* reference is ever built for any combination of Types. (and allows those
* references to be reused if a combination of types reference is requested
* a second time). This also stores the reference so that it can be
* appropriately resolved when resolveReferences() is called.
*
* It is expected that the String array used as a key to this map conforms
* to the following rules: (1) The array does not contain null values (2)
* The array does not contain redundant values (3) The array is sorted in
* alphabetical order, as defined by the natural ordering of String (for
* simplicity [and due to lack of user presentation of this value] this sort
* does not correct for internationalization)
*/
private final Map<FixedStringList, WeakReference<CDOMGroupRef<T>>> typeReferences =
new TreeMap<>(
FixedStringList.CASE_INSENSITIVE_ORDER);
/**
* Storage for individual references. This ensures that only one reference
* is ever built for any identifier. (and allows those references to be
* reused if a reference to an identifier is requested a second time). This
* also stores the reference so that it can be appropriately resolved when
* resolveReferences() is called.
*/
private final Map<String, WeakReference<CDOMSingleRef<T>>> referenced =
new TreeMap<>(
String.CASE_INSENSITIVE_ORDER);
/**
* Stores the active objects for this AbstractReferenceManufacturer. These
* are objects that have been constructed or imported into the
* AbstractReferenceManufacturer.
*/
private final KeyMap<T> active = new KeyMap<>();
/**
* Stores derivative objects (Those that are NOT created by this
* AbstractReferenceManufacturer and are NOT inserted into this
* AbstractReferenceManufacturer. However, these objects exist elsewhere,
* and need to be processed under certain conditions. The getAllObjects()
* method should NOT add this list to the items returned.
*/
private final List<T> derivatives = new ArrayList<>();
/**
* Stores the duplicate objects for identifiers in this
* AbstractReferenceManufacturer. Identifiers will only be stored in this
* Map if an identical identifier already exists in the active map. Also, if
* the gating object in the active map is removed, then the first
* "duplicate" in this MapToList should be removed and moved to the "active"
* map. This map should never contain an identifier which is not in the
* active map.
*
* Due to extremely weak .equals() rules in many PObjects, this Map MUST be
* a HashMapToInstanceList. In the future, it may be exchanged for a
* TreeMapToList that leverages String.CASE_INSENSITIVE_ORDER; however, the
* instance behavior may be too important to make that swap without
* developing a MapToList that is backed by a TreeMap and also an
* "InstanceList"
*/
private final HashMapToInstanceList<CaseInsensitiveString, T> duplicates =
new HashMapToInstanceList<>();
/**
* Contains a list of deferred objects. Identifiers for objects for which
* construction was deferred were inserted into the
* AbstractReferenceManufacturer using constructIfNecessary(String). Objects
* will be constructed when buildDeferredReferences() is called, if and only
* if no object with the matching identifier has been constructed or
* imported into this AbstractReferenceManufacturer.
*/
private final List<String> deferred = new ArrayList<>();
/**
* Contains a list of manufactured objects (those that are built implicitly
* by tokens like NATURALATTACKS). These can be "displaced" by object which
* are later built explicitly in a WeaponProf LST file, for example.
*/
private final List<WeakReference<T>> manufactured = new ArrayList<>();
/**
* The EventListenerList which contains the listeners to this
* AbstractReferenceManufacturer.
*/
private final transient EventListenerList listenerList = new EventListenerList();
/**
* Constructs a new AbstractReferenceManufacturer for the given Class.
*
* @param fac
* The ManufacturableFactory this AbstractReferenceManufacturer
* will use to construct and reference objects
* @throws IllegalArgumentException
* if the given Class is null or the given Class does not have a
* public, zero argument constructor
*
*/
public AbstractReferenceManufacturer(ManufacturableFactory<T> fac)
{
if (fac == null)
{
throw new IllegalArgumentException("Factory for "
+ getClass().getName() + " cannot be null");
}
factory = fac;
}
/**
* Gets a reference to the Class or Class/Context provided by this
* AbstractReferenceManufacturer. The reference will be a reference to the
* objects identified by the given types.
*
* @param types
* An array of the types of objects to which the returned
* CDOMReference will refer.
* @return A CDOMGroupRef which is intended to contain objects of a given
* Type for the Class or Class/Context this
* AbstractReferenceManufacturer represents.
* @throws IllegalArgumentException
* if any of the given Strings is null, empty (length is zero),
* or contains a period (.), equals (=), comma (,) or pipe (|)
*/
@Override
public CDOMGroupRef<T> getTypeReference(String... types)
{
for (String type : types)
{
if (type == null || type.isEmpty())
{
throw new IllegalArgumentException(
"Attempt to acquire empty Type "
+ "(the type String contains a null or empty element)");
}
if (type.indexOf('.') != -1)
{
throw new IllegalArgumentException(
"Cannot build Reference with type conaining a period: "
+ type);
}
if (type.indexOf('=') != -1)
{
throw new IllegalArgumentException(
"Cannot build Reference with type conaining an equals: "
+ type);
}
if (type.indexOf(',') != -1)
{
throw new IllegalArgumentException(
"Cannot build Reference with type conaining a comma: "
+ type);
}
if (type.indexOf('|') != -1)
{
throw new IllegalArgumentException(
"Cannot build Reference with type conaining a pipe: "
+ type);
}
}
Arrays.sort(types);
FixedStringList typeList = new FixedStringList(types);
WeakReference<CDOMGroupRef<T>> ref = typeReferences.get(typeList);
if (ref != null)
{
CDOMGroupRef<T> trt = ref.get();
if (trt != null)
{
return trt;
}
}
// Didn't find the appropriate key, create new
CDOMGroupRef<T> cgr = factory.getTypeReference(types);
typeReferences.put(typeList, new WeakReference<>(cgr));
return cgr;
}
/**
* Returns a CDOMGroupRef for the given Class or Class/Context provided by
* this AbstractReferenceManufacturer.
*
* @return A CDOMGroupRef which is intended to contain all the objects of
* the Class or Class/Context this AbstractReferenceManufacturer
* represents.
*/
@Override
public CDOMGroupRef<T> getAllReference()
{
if (allRef == null)
{
allRef = factory.getAllReference();
}
return allRef;
}
/**
* The class of object this AbstractReferenceManufacturer represents.
*
* @return The class of object this AbstractReferenceManufacturer
* represents.
*/
@Override
public Class<T> getReferenceClass()
{
return factory.getReferenceClass();
}
/**
* Resolves the references that have been requested from this
* AbstractReferenceManufacturer, using the objects contained within this
* AbstractReferenceManufacturer.
*
* This method guarantees that all references are resolved.
*
* Note: Implementations of AbstractReferenceManufacturer may place limits
* on the number of times resolveReferences() can be called. The reason for
* this is that some references may only be resolved once, and the
* AbstractReferenceManufacturer is not required to maintain a list of
* references that have been resolved and those which have not been
* resolved.
*/
@Override
public boolean resolveReferences(UnconstructedValidator validator)
{
boolean resolutionSuccessful = resolvePrimitiveReferences(validator);
resolutionSuccessful &= resolveGroupReferences();
for (WeakReference<CDOMGroupRef<T>> ref : typeReferences.values())
{
CDOMGroupRef<T> trt = ref.get();
if (trt != null && trt.getObjectCount() == 0)
{
Logging.errorPrint("Error: No "
+ factory.getReferenceDescription() + " objects of "
+ trt.getLSTformat(false)
+ " were loaded but were referred to in the data");
fireUnconstuctedEvent(trt);
resolutionSuccessful = false;
}
}
isResolved = true;
return resolutionSuccessful;
}
private boolean resolvePrimitiveReferences(UnconstructedValidator validator)
{
boolean resolutionSuccessful = true;
for (Entry<String, WeakReference<CDOMSingleRef<T>>> me1 : referenced.entrySet())
{
CDOMSingleRef<T> value = me1.getValue().get();
if (value != null)
{
resolutionSuccessful &= factory.resolve(this, me1.getKey(), value, validator);
}
}
return resolutionSuccessful;
}
private boolean resolveGroupReferences()
{
for (T obj : getAllObjects())
{
if (allRef != null)
{
allRef.addResolution(obj);
}
for (Map.Entry<FixedStringList, WeakReference<CDOMGroupRef<T>>> me : typeReferences
.entrySet())
{
CDOMGroupRef<T> trt = me.getValue().get();
if (trt != null)
{
boolean typeOkay = true;
for (String type : me.getKey())
{
if (!obj.isType(type))
{
typeOkay = false;
break;
}
}
if (typeOkay)
{
trt.addResolution(obj);
}
}
}
}
if (allRef != null && allRef.getObjectCount() == 0)
{
Logging.errorPrint("Error: No " + factory.getReferenceDescription()
+ " objects were loaded but were referred to in the data");
fireUnconstuctedEvent(allRef);
return false;
}
return true;
}
/**
* Adds an object to the contents of this AbstractReferenceManufacturer.
* This is used in conditions where this AbstractReferenceManufacturer was
* not used to construct the object.
*
* Implementation Note: There are various situations where this "external
* construction" may happen - the primary one being loading of "game mode"
* information like CDOMStat objects.
*
* @param item
* The object to be imported into this
* AbstractReferenceManufacturer
* @param key
* The identifier of the object to be imported into this
* AbstractReferenceManufacturer
* @throws IllegalArgumentException
* if the given object is not of the Class that this
* AbstractReferenceManufacturer constructs and references
*/
@Override
public void addObject(T item, String key)
{
if (!factory.isMember(item))
{
throw new IllegalArgumentException("Attempted to register a "
+ item.getClass().getName() + " in " + factory.getReferenceDescription());
}
T current = active.get(key);
if (current == null)
{
active.put(key, item);
}
else
{
duplicates.addToListFor(new CaseInsensitiveString(key), item);
}
}
/**
* Gets the object represented by the given identifier. Will return null if
* an object with the given identifier is not present in this
* AbstractReferenceManufacturer. Does not make any test to check if the
* given identifier has multiple matching objects.
*
* Note that this is testing *object* presence. This will not return an
* object if a reference for the given identifier has been requested; it
* will only return true if an object with the given identifier has actually
* been constructed by or imported into this AbstractReferenceManufacturer.
*
* @param key
* identifier of the object to be returned
* @return The object stored in this AbstractReferenceManufacturer with the
* given identifier, or null if this AbstractReferenceManufacturer
* does not contain an object with the given identifier.
*/
@Override
public T getActiveObject(String key)
{
return active.get(key);
}
/**
* Gets the object represented by the given identifier. Will return null if
* an object with the given identifier is not present in this
* AbstractReferenceManufacturer.
*
* Note that this is testing *object* presence. This will not return an
* object if a reference for the given identifier has been requested; it
* will only return true if an object with the given identifier has actually
* been constructed by or imported into this AbstractReferenceManufacturer.
*
* @param key
* identifier of the object to be returned
* @return The object stored in this AbstractReferenceManufacturer with the
* given identifier, or null if this AbstractReferenceManufacturer
* does not contain an object with the given identifier.
*/
@Override
public T getObject(String key)
{
T po = active.get(key);
if (po != null)
{
List<T> list = duplicates.getListFor(new CaseInsensitiveString(key));
if ((list != null) && !list.isEmpty())
{
Logging.errorPrint("Reference to Constructed "
+ factory.getReferenceDescription() + " " + key
+ " is ambiguous");
StringBuilder sb = new StringBuilder(1000);
sb.append("Locations: ");
sb.append(po.getSourceURI());
for (T dupe : list)
{
sb.append(", ");
sb.append(dupe.getSourceURI());
}
Logging.errorPrint(sb.toString());
}
return po;
}
return null;
}
/**
* Constructs a new Loadable of the Class or Class/Category represented by
* this AbstractReferenceManufacturer. This also adds the object to the list
* of constructed objects within this AbstractReferenceManufacturer.
*
* Implementation Note: At this point, the "key" provided is likely to be
* the "display name" of an object, not the actual "KEY". This is due to the
* need to construct an object at the time it is first encountered, which is
* probably not the time at which the KEY is known (the intent is not to do
* "lookahead", as it fails under .MOD conditions anyway). In order to
* "rename" an object once a KEY is encountered, see renameObject(String, T)
*
* @param key
* The identifier of the Loadable to be constructed
* @return The new Loadable of the Class or Class/Category represented by
* this AbstractReferenceManufacturer
* @throws IllegalArgumentException
* if the given identifier is null or empty (length is zero)
*/
@Override
public T constructObject(String key)
{
T obj = buildObject(key);
addObject(obj, key);
return obj;
}
/**
* Constructs a new Loadable of the Class or Class/Category represented by
* this AbstractReferenceManufacturer
*
* This should remain protected (vs. public) as it is for "internal use
* only"; it serves as a convenience method to wrap the .newInstance call
* and the possible Exceptions. Other classes should use
* constructObject(String)
*
* @param key
* The identifier of the Loadable to be constructed
* @return The new Loadable of the Class or Class/Category represented by
* this AbstractReferenceManufacturer
* @throws IllegalArgumentException
* if the given identifier is null or empty (length is zero)
*/
@Override
public T buildObject(String key)
{
if (key == null || key.equals(""))
{
throw new IllegalArgumentException("Cannot build empty name");
}
T obj = factory.newInstance();
obj.setName(key);
return obj;
}
/**
* Changes the identifier for a given object, as stored in this
* AbstractReferenceManufacturer.
*
* @param key
* The new identifier to be used for the given object
* @param item
* The object for which the identifier in this
* AbstractReferenceManufacturer should be changed
*/
@Override
public void renameObject(String key, T item)
{
String oldKey = item.getKeyName();
if (oldKey.equalsIgnoreCase(key))
{
if (Logging.isDebugMode())
{
Logging.debugPrint("Worthless Key change encountered: "
+ item.getDisplayName() + " " + oldKey);
Logging.reportSource(Logging.DEBUG, item.getSourceURI());
}
}
forgetObject(item);
addObject(item, key);
}
/**
* Remove the given object from this AbstractReferenceManufacturer. Returns
* true if the object was removed from this AbstractReferenceManufacturer;
* false otherwise.
*
* @param item
* The object to be removed from this
* AbstractReferenceManufacturer.
* @return true if the object was removed from this
* AbstractReferenceManufacturer; false otherwise.
*/
@Override
public boolean forgetObject(T item)
{
if (!factory.isMember(item))
{
throw new IllegalArgumentException(
"Object to be forgotten does not match Class "
+ "of this AbstractReferenceManufacturer");
}
String key = active.getKeyFor(item);
if (key == null)
{
/*
* TODO This is a bug - the key name is not necessarily loaded into
* the object, it may have been consumed by the object context... :P
*/
CaseInsensitiveString ocik = new CaseInsensitiveString(item
.getKeyName());
duplicates.removeFromListFor(ocik, item);
}
else
{
CaseInsensitiveString ocik = new CaseInsensitiveString(key);
List<T> list = duplicates.getListFor(ocik);
if (list == null)
{
// No replacement
active.remove(key);
}
else
{
T newActive = duplicates.getElementInList(ocik, 0);
duplicates.removeFromListFor(ocik, newActive);
active.put(key, newActive);
}
}
return true;
}
/**
* Returns true if this AbstractReferenceManufacturer contains an object of
* the Class or Class/Category represented by this
* AbstractReferenceManufacturer.
*
* Note that this is testing *object* presence. This will not return true if
* a reference for the given identifier has been requested; it will only
* return true if an object with the given identifier has actually been
* constructed by or imported into this AbstractReferenceManufacturer.
*
* @param key
* The identifier of the object to be checked if it is present in
* this AbstractReferenceManufacturer.
* @return true if this AbstractReferenceManufacturer contains an object of
* the Class or Class/Category represented by this
* AbstractReferenceManufacturer; false otherwise.
*/
@Override
public boolean containsObject(String key)
{
return active.containsKey(key);
}
/**
* Gets a reference to the Class or Class/Context provided by this
* AbstractReferenceManufacturer. The reference will be a reference to the
* object identified by the given key.
*
* @param key
* The key used to identify the object to which the returned
* CDOMReference will refer.
* @return A CDOMReference that refers to the object identified by the given
* key
* @throws IllegalArgumentException
* if the given key is null or empty
*/
@Override
public CDOMSingleRef<T> getReference(String key)
{
/*
* TODO This is incorrect, but a hack for now :)
*
* Mainly this throws around IllegalArgumentException in order to catch
* bad parsing issues (design flaws in the code). Not sure if we want to
* continue that long term? Once tokens are truly tested this may not be
* necessary or desirable.
*/
if (key == null)
{
throw new IllegalArgumentException(
"Cannot request a reference to null identifier");
}
if (key.isEmpty())
{
throw new IllegalArgumentException(
"Cannot request a reference to an empty identifier");
}
/*
* Items thrown below this point are for protection from coding errors
* in LST files, not part of the public interface of this method
*/
try
{
Integer.parseInt(key);
throw new IllegalArgumentException("A number cannot be a valid single item: " + key);
}
catch (NumberFormatException nfe)
{
// ok
}
if (key.contains("="))
{
throw new IllegalArgumentException(
"= cannot be a in valid single item (perhaps something like TYPE= "
+ "is not supported in this token?): " + key);
}
if (key.equalsIgnoreCase("ANY"))
{
throw new IllegalArgumentException(
"Any cannot be a valid single item (not supported in this token?)");
}
if (key.equalsIgnoreCase("ALL"))
{
throw new IllegalArgumentException(
"All cannot be a valid single item (not supported in this token?)");
}
if (key.contains(":"))
{
throw new IllegalArgumentException(
": cannot exist in a valid single item (did you try to use a "
+ "PRE where it is not supported?) " + key);
}
if (key.equalsIgnoreCase("%LIST"))
{
throw new IllegalArgumentException(
"%LIST cannot be a valid single item (not supported in this token?)");
}
WeakReference<CDOMSingleRef<T>> wr = referenced.get(key);
if (wr != null)
{
CDOMSingleRef<T> ref = wr.get();
if (ref != null)
{
return ref;
}
}
CDOMSingleRef<T> ref;
if (isResolved)
{
T current = active.get(key);
if (current == null)
{
throw new IllegalArgumentException(key
+ " is not valid post-resolution "
+ "because it was never constructed");
}
ref = CDOMDirectSingleRef.getRef(current);
}
else
{
CDOMSingleRef<T> lr = factory.getReference(key);
referenced.put(key, new WeakReference<>(lr));
ref = lr;
}
return ref;
}
/**
* Returns true if this AbstractReferenceManufacturer is "valid". A "valid"
* AbstractReferenceManufacturer is one where all of the following are true:
*
* (1) Any object stored in the AbstractReferenceManufacturer reports that
* it's KEY (as defined by Loadable.getKeyName()) matches the identifier
* used to store the object in the AbstractReferenceManufacturer.
*
* (2) All objects stored in the ReferenceManufacturer have valid names
* (do not use illegal characters in the names)
*
* (3) No two objects in the AbstractReferenceManufacturer have a matching
* identifier.
*
* @param validator
* UnconstructedValidator which can suppress unconstructed
* reference warnings
*
* @return true if the AbstractReferenceManufacturer is "valid"; false
* otherwise.
*/
@Override
public boolean validate(UnconstructedValidator validator)
{
boolean returnGood = true;
if (validator == null
|| !validator.allowDuplicates(getReferenceClass()))
{
returnGood = validateDuplicates();
}
returnGood &= validateNames();
returnGood &= validateActive();
return returnGood;
}
private boolean validateNames()
{
if (!Logging.isLoggable(Logging.LST_WARNING))
{
return true;
}
for (String key : active.keySet())
{
T value = active.get(key);
if (value.isInternal())
{
continue;
}
/*
* http://wiki.pcgen.org/index.php?title=Data_LST_Standards
*
* Characters which should never be used in object names are Commas
* (,), Pipes (|), Backslashes (\), Colons (:), Semicolons (;),
* Periods (.), Brackets ([]), Percent (%), Asterisk (*) and Equals
* (=).
*/
if (key.indexOf(',') != -1 && factory.getReferenceClass() != RollMethod.class)
{
Logging.log(Logging.LST_WARNING, "Found "
+ factory.getReferenceDescription() + " with KEY: " + key
+ " which contains a comma "
+ "(prohibited character in a key)");
}
if (key.indexOf('|') != -1)
{
Logging.log(Logging.LST_WARNING, "Found "
+ factory.getReferenceDescription() + " with KEY: " + key
+ " which contains a pipe "
+ "(prohibited character in a key)");
}
if (key.indexOf('\\') != -1)
{
Logging.log(Logging.LST_WARNING, "Found "
+ factory.getReferenceDescription() + " with KEY: " + key
+ " which contains a backslash "
+ "(prohibited character in a key)");
}
if (key.indexOf(':') != -1)
{
Logging.log(Logging.LST_WARNING, "Found "
+ factory.getReferenceDescription() + " with KEY: " + key
+ " which contains a colon "
+ "(prohibited character in a key)");
}
if (key.indexOf(';') != -1)
{
Logging.log(Logging.LST_WARNING, "Found "
+ factory.getReferenceDescription() + " with KEY: " + key
+ " which contains a semicolon "
+ "(prohibited character in a key)");
}
// if (key.indexOf('.') != -1)
// {
// Logging.log(Logging.LST_WARNING, "Found "
// + getReferenceDescription() + " with KEY: " + key
// + " which contains a period "
// + "(prohibited character in a key)");
// }
if (key.indexOf('%') != -1)
{
Logging.log(Logging.LST_WARNING, "Found "
+ factory.getReferenceDescription() + " with KEY: " + key
+ " which contains a percent sign "
+ "(prohibited character in a key)");
}
if (key.indexOf('*') != -1)
{
Logging.log(Logging.LST_WARNING, "Found "
+ factory.getReferenceDescription() + " with KEY: " + key
+ " which contains an asterisk "
+ "(prohibited character in a key)");
}
if (key.indexOf('=') != -1)
{
Logging.log(Logging.LST_WARNING, "Found "
+ factory.getReferenceDescription() + " with KEY: " + key
+ " which contains an equals sign "
+ "(prohibited character in a key)");
}
if ((key.indexOf('[') != -1) || (key.indexOf(']') != -1))
{
Logging.log(Logging.LST_WARNING, "Found "
+ factory.getReferenceDescription() + " with KEY: " + key
+ " which contains a bracket "
+ "(prohibited character in a key)");
}
}
return true;
}
private boolean validateActive()
{
boolean returnGood = true;
for (Object second : active.keySet())
{
T activeObj = active.get(second);
String keyName = activeObj.getKeyName();
if (keyName == null)
{
Logging.errorPrint(activeObj.getClass() + " "
+ activeObj.getDisplayName() + " has a null KeyName");
}
else if (!keyName.equalsIgnoreCase(second.toString()))
{
Logging.errorPrint(getReferenceDescription()
+ " Magical Key Change: " + second + " to " + keyName);
returnGood = false;
}
}
return returnGood;
}
private boolean validateDuplicates()
{
boolean returnGood = true;
for (CaseInsensitiveString second : duplicates.getKeySet())
{
List<T> list = duplicates.getListFor(second);
T good = active.get(second.toString());
/*
* CONSIDER Should get CDOMObject reference out of here :(
*/
if (good instanceof CDOMObject)
{
CDOMObject cdo = (CDOMObject) good;
for (int i = 0; i < list.size(); i++)
{
T dupe = list.get(i);
if (cdo.isCDOMEqual((CDOMObject) dupe))
{
for (Iterator<WeakReference<T>> it = manufactured
.iterator(); it.hasNext();)
{
WeakReference<T> wr = it.next();
T mfg = wr.get();
if (mfg == null)
{
it.remove();
}
// Yes this is instance equality, not .equals
else if (mfg == good)
{
forgetObject(good);
break;
}
}
}
}
}
if (duplicates.containsListFor(second))
{
Logging.errorPrint("More than one " + factory.getReferenceDescription()
+ " with key/name " + good.getKeyName() + " was built");
List<T> dupes = duplicates.getListFor(second);
StringBuilder sb = new StringBuilder(1000);
sb.append("Sources: ");
sb.append(good.isInternal() ? "<internal>" : good.getSourceURI());
for (T dupe : dupes)
{
sb.append(", ").append(dupe.isInternal() ? "<internal>" : dupe.getSourceURI());
if (!dupe.getKeyName().equals(good.getKeyName()))
{
Logging.errorPrint("Key case differed for " + dupe.getKeyName());
}
}
Logging.errorPrint(sb.toString());
returnGood = false;
}
}
return returnGood;
}
/**
* Instructs the AbstractReferenceManufacturer that the object with the
* given identifer should be constructed automatically if it is necessary
* when buildDeferredObjects() is called. The object will be constructed
* only if no object with the matching identifier has been constructed or
* imported into this AbstractReferenceManufacturer.
*
* Implementation Note: This is generally used for backwards compatibility
* to previous versions of PCGen or to items that are built automatically
* (such as Weapon Proficiencies for Natural Attacks)
*
* @param key
* The identifier of the Loadable to be built (if otherwise not
* constructed or imported into this
* AbstractReferenceManufacturer) when buildDeferredObjects() is
* called.
*/
@Override
public void constructIfNecessary(String key)
{
/*
* TODO FIXME Need to ensure that items that are built here are tagged
* as manufactured, so that they are not written out to LST files
*/
deferred.add(key);
}
/**
* Returns a Collection of all of the objects contained in this
* AbstractReferenceManufacturer, sorted by their Key Name. This will not
* return null, it will return an empty list if no objects have been
* constructed by or imported into this AbstractReferenceManufacturer.
*
* @return A sorted Collection of all of the objects contained in this
* AbstractReferenceManufacturer
*/
@Override
public Collection<T> getAllObjects()
{
return active.keySortedValues();
}
/**
* Returns a List of all of the objects contained in this
* AbstractReferenceManufacturer in the original order in which they were
* imported into this AbstractReferenceManufacturer. This will not return
* null, it will return an empty list if no objects have been constructed by
* or imported into this AbstractReferenceManufacturer.
*
* @return A List of all of the objects contained in this
* AbstractReferenceManufacturer
*/
@Override
public List<T> getOrderSortedObjects()
{
return active.insertOrderValues();
}
/**
* Builds any objects whose construction was deferred. Identifiers for
* objects for which construction was deferred were inserted into the
* AbstractReferenceManufacturer using constructIfNecessary(String). Objects
* will be constructed only if no object with the matching identifier has
* been constructed or imported into this AbstractReferenceManufacturer.
*
* Construction or import into the AbstractReferenceManufacturer could occur
* at any time before buildDeferredObjects() is called, either before or
* after constructIfNecessary(String) was called with the relevant
* identifier. However, construction or import of an object with an
* identical identifier after buildDeferredObjects() is called will result
* in a duplicate object being formed. AbstractReferenceManufacturer is not
* responsible for deleting automatically built objects under those
* conditions.
*/
@Override
public void buildDeferredObjects()
{
for (String cis : deferred)
{
if (!active.containsKey(cis))
{
constructObject(cis);
}
}
}
/**
* Returns a Collection of the "TYPE" references for this
* AbstractReferenceManufacturer.
*
* This method is value-semantic in that ownership of the returned
* Collection is transferred to the class calling this method. Modification
* of the returned Collection will not modify the "TYPE" references for this
* AbstractReferenceManufacturer and modification of the "TYPE" references
* for this AbstractReferenceManufacturer through subsequent calls of
* getTypeReference(String...) will not modify the returned Collection.
*
* This method will not return null, even if getTypeReference(String...)
* method was never called.
*
* @return A Collection of the "TYPE" references for this
* AbstractReferenceManufacturer.
*/
protected Collection<CDOMGroupRef<T>> getTypeReferences()
{
List<CDOMGroupRef<T>> list = new ArrayList<>(typeReferences.size());
for (Iterator<WeakReference<CDOMGroupRef<T>>> it = typeReferences.values()
.iterator(); it.hasNext();)
{
WeakReference<CDOMGroupRef<T>> wr = it.next();
CDOMGroupRef<T> trt = wr.get();
if (trt == null)
{
it.remove();
}
else
{
list.add(trt);
}
}
return list;
}
/**
* Returns a Collection of the primitive references for this
* AbstractReferenceManufacturer.
*
* This method is value-semantic in that ownership of the returned
* Collection is transferred to the class calling this method. Modification
* of the returned Collection will not modify the primitive references for
* this AbstractReferenceManufacturer and modification of the primitive
* references for this AbstractReferenceManufacturer through subsequent
* calls of getReference(String) will not modify the returned Collection.
*
* This method will not return null, even if getReference(String) method was
* never called.
*
* @return A Collection of the primitive references for this
* AbstractReferenceManufacturer.
*/
@Override
public Collection<CDOMSingleRef<T>> getReferenced()
{
List<CDOMSingleRef<T>> list = new ArrayList<>();
for (WeakReference<CDOMSingleRef<T>> wr : referenced.values())
{
CDOMSingleRef<T> ref = wr.get();
if (ref != null)
{
list.add(ref);
}
}
return list;
}
/**
* Injects all objects from the given ReferenceManufacturer into this
* AbstractReferenceManufacturer. Effectively this is a bulk addObject for
* all of the objects contained in the given ReferenceManufacturer.
*
* Note that this imports only the objects, and NOT references. This
* AbstractReferenceManufacturer does inherit any deferred objects
* (triggered through constructIfNecessary) from the given
* ReferenceManufacturer.
*
* @param arm
* The ReferenceManufacturer from which the objects should be
* imported into this AbstractReferenceManufacturer
*/
@Override
public void injectConstructed(ReferenceManufacturer<T> arm)
{
// Must maintain order
for (T value : active.insertOrderValues())
{
arm.addObject(value, active.getKeyFor(value));
}
for (CaseInsensitiveString cis : duplicates.getKeySet())
{
for (T obj : duplicates.getListFor(cis))
{
arm.addObject(obj, cis.toString());
}
}
for (String s : deferred)
{
arm.constructIfNecessary(s);
}
}
/**
* Triggers immediate construction of the object with the given identifier
* if it does not exist. This is an alternative to constructIfNecessary that
* should be used sparingly (generally direct access like this is higher
* risk, but necessary in some cases)
*
* Note that use of this method is inherently risky when taken in context to
* .MOD and .COPY. Changes to keys may change the object to which an
* identifier refers. Therefore, any resolution that should take place at
* runtime should use getReference and resolve the reference.
*
* The object will be constructed only if no object with the matching
* identifier has been constructed or imported into this
* ReferenceManufacturer. If the object has already been constructed, then
* the previously constructed object is returned.
*
* This method is effectively a convenience method that wraps
* containsObject, getObject, and constructObject into a single method call
* (and avoids the contains-triggered branch)
*
* @param key
* The identifier of the Loadable to be built (if otherwise not
* constructed or imported into this
* AbstractReferenceManufacturer), or if an object with that
* identifier already exists, the identifier of the object to be
* returned.
* @return The previously existing or new Loadable with the given
* identifier.
*/
@Override
public T constructNowIfNecessary(String key)
{
T obj = active.get(key);
if (obj == null)
{
obj = constructObject(key);
manufactured.add(new WeakReference<>(obj));
}
return obj;
}
/**
* Adds an UnconstructedListener to this AbstractReferenceManufacturer, that
* will receive UnconstructedEvents if the validate method of this
* AbstractReferenceManufacturer is called and the UnconstructedValidator
* given to the validate method does not report that the unconstructed
* reference is permitted.
*
* @param listener
* The UnconstructedListener to be registered with this
* AbstractReferenceManufacturer
*/
@Override
public void addUnconstructedListener(UnconstructedListener listener)
{
listenerList.add(UnconstructedListener.class, listener);
}
/**
* Returns an array of UnconstructedListeners that are registered with this
* AbstractReferenceManufacturer.
*
* @return An array of UnconstructedListeners that are registered with this
* AbstractReferenceManufacturer.
*/
@Override
public synchronized UnconstructedListener[] getUnconstructedListeners()
{
return listenerList.getListeners(UnconstructedListener.class);
}
/**
* Removes an UnconstructedListener from this AbstractReferenceManufacturer,
* so that it will no longer receive UnconstructedEvents from this
* AbstractReferenceManufacturer
*
* @param listener
* The UnconstructedListener to be removed from registration with
* this AbstractReferenceManufacturer
*/
@Override
public void removeUnconstructedListener(UnconstructedListener listener)
{
listenerList.remove(UnconstructedListener.class, listener);
}
/**
* Fires a new UnconstructedEvent for the given CDOMReference to any
* UnconstructedListener objects registered with this
* AbstractReferenceManufacturer
*
* @param ref
* The CDOMReference to which the UnconstructedEvent should
* refer.
*/
@Override
public void fireUnconstuctedEvent(CDOMReference<?> ref)
{
Object[] listeners = listenerList.getListenerList();
/*
* This list is decremented from the end of the list to the beginning in
* order to maintain consistent operation with how Java AWT and Swing
* listeners are notified of Events (they are in reverse order to how
* they were added to the Event-owning object).
*/
UnconstructedEvent uEvent = null;
for (int i = listeners.length - 2; i >= 0; i -= 2)
{
if (listeners[i] == UnconstructedListener.class)
{
// Lazily create event
if (uEvent == null)
{
uEvent = new UnconstructedEvent(this, ref); // NOPMD
}
((UnconstructedListener) listeners[i + 1])
.unconstructedReferenceFound(uEvent);
}
}
}
/**
* Returns the number of objects that are constructed in this
* AbstractReferenceManufacturer (These could be natively constructed or
* imported objects and does not count duplicates)
*
* @return The number of objects that are constructed in this
* AbstractReferenceManufacturer
*/
@Override
public int getConstructedObjectCount()
{
return active.size();
}
@Override
public T getItemInOrder(int index)
{
return active.getItemInOrder(index);
}
@Override
public ManufacturableFactory<T> getFactory()
{
return factory;
}
@Override
public String getReferenceDescription()
{
return factory.getReferenceDescription();
}
@Override
public Collection<CDOMReference<T>> getAllReferences()
{
List<CDOMReference<T>> list = new ArrayList<>();
if (allRef != null)
{
list.add(allRef);
}
list.addAll(getTypeReferences());
list.addAll(getReferenced());
return list;
}
@Override
public void addDerivativeObject(T obj)
{
if (obj == null)
{
throw new IllegalArgumentException("Derivative Object cannot be null");
}
derivatives.add(obj);
}
@Override
public Collection<T> getDerivativeObjects()
{
return new ArrayList<>(derivatives);
}
@Override
public T convert(String key)
{
return getActiveObject(key);
}
@Override
public Indirect<T> convertIndirect(String key)
{
return factory.getReference(key);
}
@Override
public String getIdentifierType()
{
return StringPClassUtil.getStringFor(getManagedClass());
}
@Override
public Class<T> getManagedClass()
{
return factory.getReferenceClass();
}
@Override
public String unconvert(T arg0)
{
return arg0.getKeyName();
}
@Override
@SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract")
public FormatManager<?> getComponentManager()
{
return null;
}
}