/* * LstLineFileLoader.java * Copyright 2003 (C) David Hibbs <sage_sam@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 * * Created on November 17, 2003, 12:00 PM * * Current Ver: $Revision$ <br> */ package pcgen.persistence.lst; import java.net.URI; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Observable; import java.util.Set; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.enumeration.StringKey; import pcgen.core.Campaign; import pcgen.persistence.PersistenceLayerException; import pcgen.rules.context.LoadContext; import pcgen.util.Logging; import pcgen.system.LanguageBundle; import pcgen.system.PCGenSettings; /** * This class is an extension of the LstFileLoader that loads items * that are CDOMObjects and have a source campaign associated with them. * Objects loaded by implementations of this class inherit the core * MOD/COPY/FORGET funcationality needed for core CDOMObjects used * to directly create characters. * * <p> * Current Ver: $Revision$ <br> * * @author AD9C15 * @author boomer70 <boomer70@yahoo.com> */ public abstract class LstObjectFileLoader<T extends CDOMObject> extends Observable { /** The String that separates fields in the file. */ public static final String FIELD_SEPARATOR = "\t"; //$NON-NLS-1$ /** The String that separates individual objects */ public static final String LINE_SEPARATOR = "\r\n"; //$NON-NLS-1$ /** Tag used to include an object */ public static final String INCLUDE_TAG = "INCLUDE"; //$NON-NLS-1$ /** Tag used to exclude an object */ public static final String EXCLUDE_TAG = "EXCLUDE"; //$NON-NLS-1$ /** The suffix used to indicate this is a copy operation */ public static final String COPY_SUFFIX = ".COPY"; //$NON-NLS-1$ /** The suffix used to indicate this is a mod operation */ public static final String MOD_SUFFIX = ".MOD"; //$NON-NLS-1$ /** The suffix used to indicate this is a forget operation */ public static final String FORGET_SUFFIX = ".FORGET"; //$NON-NLS-1$ private List<ModEntry> copyLineList = new ArrayList<>(); private List<String> forgetLineList = new ArrayList<>(); private List<List<ModEntry>> modEntryList = new ArrayList<>(); private boolean processComplete = true; /** A list of objects that will not be included. */ protected List<String> excludedObjects = new ArrayList<>(); /** * LstObjectFileLoader constructor. */ public LstObjectFileLoader() { super(); } /** * This method loads the given list of LST files. * @param fileList containing the list of files to read * @throws PersistenceLayerException */ public void loadLstFiles(LoadContext context, List<CampaignSourceEntry> fileList) throws PersistenceLayerException { processComplete = true; // Track which sources have been loaded already Set<CampaignSourceEntry> loadedFiles = new HashSet<>(); // Load the files themselves as thoroughly as possible for (CampaignSourceEntry sourceEntry : fileList) { if (sourceEntry == null) { continue; } // Check if the CSE has already been loaded before loading it if (!loadedFiles.contains(sourceEntry)) { loadLstFile(context, sourceEntry); loadedFiles.add(sourceEntry); } } // Next we perform copy operations processCopies(context); // Now handle .MOD items processComplete = false; processMods(context); // Finally, forget the .FORGET items processForgets(context); } /** * This method parses the LST file line, applying it to the provided target * object. If the line indicates the start of a new target object, a new * CDOMObject of the appropriate type will be created prior to applying the * line contents. Because of this behavior, it is necessary for this * method to return the new object. Implementations of this method also * MUST call {@code completeObject} with the original target prior to * returning the new value. * @param context TODO * @param target CDOMObject to apply the line to, barring the start of a * new object * @param lstLine String LST formatted line read from the source URL * @param source SourceEntry indicating the file that the line was * read from as well as the Campaign object that referenced the file * * @return CDOMObject that was either created or modified by the provided * LST line * @throws PersistenceLayerException if there is a problem with the LST syntax */ public abstract T parseLine(LoadContext context, T target, String lstLine, SourceEntry source) throws PersistenceLayerException; /** * This method is called by the loading framework to signify that the * loading of this object is complete and the object should be added to the * system. * * <p>This method will check that the loaded object should be included via * a call to {@code includeObject} and if not add it to the list of * excluded objects. * * <p>Once the object has been verified the method will call * {@code finishObject} to give each object a chance to complete * processing. * * <p>The object is then added to the system if it doesn't already exist. * If the object exists, the object sources are compared by date and if the * System setting allowing over-rides is set it will use the object from the * newer source. * @param context TODO * @param pObj The object that has just completed loading. * * @see pcgen.persistence.lst.LstObjectFileLoader#includeObject(SourceEntry, CDOMObject) * * @throws PersistenceLayerException */ public void completeObject(LoadContext context, SourceEntry source, final T pObj) throws PersistenceLayerException { if (!processComplete || pObj == null) { return; } if (includeObject(source, pObj)) { storeObject(context, pObj); } else { excludedObjects.add(pObj.getKeyName()); context.getReferenceContext().forget(pObj); } } protected void storeObject(LoadContext context, T pObj) { final T currentObj = getMatchingObject(context, pObj); if (!context.consolidate() || currentObj == null || !pObj.equals(currentObj)) { addGlobalObject(pObj); } else { //Yes, this is instance equality, NOT .equals!!!!! if (currentObj != pObj) { boolean allowoverride = PCGenSettings.OPTIONS_CONTEXT.initBoolean( PCGenSettings.OPTION_ALLOW_OVERRIDE_DUPLICATES, true); if (allowoverride) { // If the new object is more recent than the current // one, use the new object final Date pObjDate = pObj.get(ObjectKey.SOURCE_DATE); final Date currentObjDate = currentObj .get(ObjectKey.SOURCE_DATE); if ((pObjDate != null) && ((currentObjDate == null) || ((pObjDate .compareTo(currentObjDate) > 0)))) { performForget(context, currentObj); addGlobalObject(pObj); } else { /* * This does not use performForget since this is only * forgetting something that is local to the context * (was never "added" to the 5.x system) */ context.getReferenceContext().forget(pObj); } } else { // Duplicate loading error Logging.errorPrintLocalised( "Warnings.LstFileLoader.DuplicateObject", //$NON-NLS-1$ pObj.getKeyName(), currentObj.getSourceURI(), pObj .getSourceURI()); } } } } /** * Adds an object to the global repository. * * @param cdo The object to add. * */ protected void addGlobalObject(final CDOMObject cdo) { } /** * This method should be called by finishObject implementations in * order to check if the parsed object is affected by an INCLUDE or * EXCLUDE request. * * @param cdo CDOMObject to determine whether to include in * Globals etc. * @return boolean true if the object should be included, else false * to exclude it */ protected boolean includeObject(SourceEntry source, CDOMObject cdo) { // Null check; never add nulls or objects without a name/key name if ((cdo == null) || (cdo.getDisplayName() == null) || (cdo.getDisplayName().trim().isEmpty()) || (cdo.getKeyName() == null) || (cdo.getKeyName().trim().isEmpty())) { return false; } // If includes were present, check includes for given object List<String> includeItems = source.getIncludeItems(); if (!includeItems.isEmpty()) { return includeItems.contains(cdo.getKeyName()); } // If excludes were present, check excludes for given object List<String> excludeItems = source.getExcludeItems(); if (!excludeItems.isEmpty()) { return !excludeItems.contains(cdo.getKeyName()); } return true; } /** * This method retrieves a CDOMObject from globals by its key. * This is used to avoid duplicate loads, get objects to forget or * modify, etc. * @param context TODO * @param aKey String key of CDOMObject to retrieve * @return CDOMObject of the given key */ protected abstract T getObjectKeyed(LoadContext context, String aKey); /** * This method retrieves a CDOMObject from the global list, attempting to match (by key * and category, if necessary), the given object. This is used to avoid * duplicate loads * @param context TODO * @param key The CDOMObject containing the key to retrieve (for which there may be a duplicate) * * @return CDOMObject from Globals */ protected T getMatchingObject(LoadContext context, CDOMObject key) { return getObjectKeyed(context, key.getKeyName()); } /** * This method loads a single LST formatted file. * @param sourceEntry CampaignSourceEntry containing the absolute file path * or the URL from which to read LST formatted data. */ protected void loadLstFile(LoadContext context, CampaignSourceEntry sourceEntry) { setChanged(); URI uri = sourceEntry.getURI(); notifyObservers(uri); StringBuilder dataBuffer; try { dataBuffer = LstFileLoader.readFromURI(uri); } catch (PersistenceLayerException ple) { String message = LanguageBundle.getFormattedString( "Errors.LstFileLoader.LoadError", //$NON-NLS-1$ uri, ple.getMessage()); Logging.errorPrint(message); setChanged(); return; } String aString = dataBuffer.toString(); if (context != null) { context.setSourceURI(uri); } T target = null; ArrayList<ModEntry> classModLines = null; boolean allowMultiLine = PCGenSettings.OPTIONS_CONTEXT.initBoolean( PCGenSettings.OPTION_SOURCES_ALLOW_MULTI_LINE, false); if (allowMultiLine) { // Support the new file type. All lines that start with a tab belong to the previous line. aString = aString.replaceAll("\r?\n\t", "\t"); } String[] fileLines = aString.split(LstFileLoader.LINE_SEPARATOR_REGEXP); for (int i = 0; i < fileLines.length; i++) { String line = fileLines[i]; if ((line.length() == 0) || (line.charAt(0) == LstFileLoader.LINE_COMMENT_CHAR)) { continue; } int sepLoc = line.indexOf(FIELD_SEPARATOR); String firstToken; if (sepLoc == -1) { firstToken = line; } else { firstToken = line.substring(0, sepLoc); } // Check for continuation of class mods if (classModLines != null) { // TODO - Figure out why we need to check CLASS: in this file. if (firstToken.startsWith("CLASS:")) //$NON-NLS-1$ { modEntryList.add(classModLines); classModLines = null; } else { // Add the line to the class mod and don't process it yet. classModLines.add(new ModEntry(sourceEntry, line, i + 1)); continue; } } // check for copies, mods, and forgets // TODO - Figure out why we need to check SOURCE in this file if (line.startsWith("SOURCE")) //$NON-NLS-1$ { SourceLoader.parseLine(context, line, uri); } else if (line.trim().length()==0) { // Ignore the line } else if (firstToken.indexOf(COPY_SUFFIX) > 0) { copyLineList.add(new ModEntry(sourceEntry, line, i + 1)); } else if (firstToken.indexOf(MOD_SUFFIX) > 0) { // TODO - Figure out why we need to check CLASS: in this file. if (firstToken.startsWith("CLASS:")) //$NON-NLS-1$ { // As CLASS:abc.MOD can be followed by level lines, we place the // lines into a list for processing in a group afterwards classModLines = new ArrayList<>(); classModLines.add(new ModEntry(sourceEntry, line, i + 1)); } else { List<ModEntry> modLines = new ArrayList<>(1); modLines.add(new ModEntry(sourceEntry, line, i + 1)); modEntryList.add(modLines); } } else if (firstToken.indexOf(FORGET_SUFFIX) > 0) { forgetLineList.add(line); } else { try { target = parseLine(context, target, line, sourceEntry); } catch (PersistenceLayerException ple) { String message = LanguageBundle.getFormattedString( "Errors.LstFileLoader.ParseError", //$NON-NLS-1$ uri, i + 1, ple.getMessage()); Logging.errorPrint(message); setChanged(); if (Logging.isDebugMode()) { Logging.debugPrint("Parse error:", ple); //$NON-NLS-1$ } } catch (Throwable t) { String message = LanguageBundle.getFormattedString( "Errors.LstFileLoader.ParseError", //$NON-NLS-1$ uri, i + 1, t.getMessage()); Logging.errorPrint(message, t); setChanged(); Logging.errorPrint(LanguageBundle .getString("Errors.LstFileLoader.Ignoring: " + t.getMessage())); if (Logging.isDebugMode()) { Logging.errorPrint(LanguageBundle .getString("Errors.LstFileLoader.Ignoring"), t); t.printStackTrace(); } } } } if (classModLines != null) { modEntryList.add(classModLines); } if (target != null) { try { completeObject(context, sourceEntry, target); } catch (PersistenceLayerException ple) { Logging.errorPrint("Error in completing " + target.getClass().getSimpleName() + " " + target.getKeyName()); setChanged(); if (Logging.isDebugMode()) { Logging.debugPrint("Parse error:", ple); //$NON-NLS-1$ } } } } /** * This method, when implemented, will perform a single .FORGET * operation. * @param context TODO * @param objToForget containing the object to forget */ protected void performForget(LoadContext context, T objToForget) { context.getReferenceContext().forget(objToForget); } /** * This method will perform a single .COPY operation based on the LST * file content. * @param lstLine String containing the LST source for the * .COPY operation * @throws PersistenceLayerException */ private void performCopy(LoadContext context, ModEntry me) throws PersistenceLayerException { String lstLine = me.getLstLine(); int sepLoc = lstLine.indexOf(FIELD_SEPARATOR); String name; if (sepLoc != -1) { name = lstLine.substring(0, sepLoc); } else { name = lstLine; } final int nameEnd = name.indexOf(COPY_SUFFIX); final String baseName = name.substring(0, nameEnd); final String copyName = name.substring(nameEnd + 6); T copy = getCopy(context, baseName, copyName.intern(), me.source); if (copy != null) { if (sepLoc != -1) { String restOfLine = me.getLstLine().substring(nameEnd + 6); parseLine(context, copy, restOfLine, me.getSource()); } completeObject(context, me.getSource(), copy); } } /** * Create a copy of an object with a new name. If the base object cannot be found, an error will be reported unless * the copy has been excluded by include/exclude rules for the source. * * @param context The current load context in whihc the new object is to be created. * @param baseName The name of the object to be copied. * @param copyName The name of the new object. * @param source The source containing the copy. * @return The new object, or null if the base object could not be found. * @throws PersistenceLayerException If an unexpected error occurs. */ protected T getCopy(LoadContext context, final String baseName, final String copyName, CampaignSourceEntry source) throws PersistenceLayerException { T object = getObjectKeyed(context, baseName); if (object == null) { List<String> includeItems = source.getIncludeItems(); if (!includeItems.isEmpty() && !includeItems.contains(copyName)) { return null; } List<String> excludeItems = source.getExcludeItems(); if (excludeItems.contains(copyName)) { return null; } String message = LanguageBundle.getFormattedString( "Errors.LstFileLoader.CopyObjectNotFound", //$NON-NLS-1$ baseName); Logging.errorPrint(message); setChanged(); return null; } T obj = context.performCopy(object, copyName); if (obj == null) { setChanged(); } return obj; } /** * This method will perform a multi-line .MOD operation. This is used * for example in MODs of CLASSES which can have multiple lines. Loaders * can [typically] use the name without checking * for (or stripping off) .MOD due to the implementation of * CDOMObject.setName() * @param entryList */ private void performMod(LoadContext context, List<ModEntry> entryList) { ModEntry entry = entryList.get(0); // get the name of the object to modify, trimming off the .MOD int nameEnd = entry.getLstLine().indexOf(MOD_SUFFIX); String key = entry.getLstLine().substring(0, nameEnd); List<String> includeItems = entry.source.getIncludeItems(); // remove the leading tag, if any (i.e. CLASS:Druid.MOD int nameStart = key.indexOf(':'); if (nameStart > 0) { key = key.substring(nameStart + 1); } // get the actual object to modify T object = context.getReferenceContext().performMod(getObjectKeyed(context, key)); if (object == null) { if (!includeItems.isEmpty() && !includeItems.contains(key)) { return; } if (excludedObjects.contains(key)) { return; } String message = LanguageBundle.getFormattedString( "Errors.LstFileLoader.ModObjectNotFound", //$NON-NLS-1$ entry.getSource().getURI(), entry.getLineNumber(), key); Logging.log(Logging.LST_ERROR, message); setChanged(); return; } // modify the object try { if (includeItems.isEmpty() || includeItems.contains(key)) { for (ModEntry element : entryList) { context.setSourceURI(element.source.getURI()); try { String origPage = object.get(StringKey.SOURCE_PAGE); parseLine(context, object, element.getLstLine(), element.getSource()); if (origPage != object.get(StringKey.SOURCE_PAGE)) { Campaign campaign = element.source.getCampaign(); object.put(ObjectKey.SOURCE_CAMPAIGN, campaign); object.put(StringKey.SOURCE_SHORT, campaign.get(StringKey.SOURCE_SHORT)); object.put(StringKey.SOURCE_LONG, campaign.get(StringKey.SOURCE_LONG)); object.put(ObjectKey.SOURCE_DATE, campaign.get(ObjectKey.SOURCE_DATE)); object.put(StringKey.SOURCE_WEB, campaign.get(StringKey.SOURCE_WEB)); object.setSourceURI(element.source.getURI()); } } catch (PersistenceLayerException ple) { String message = LanguageBundle.getFormattedString( "Errors.LstFileLoader.ModParseError", //$NON-NLS-1$ element.getSource().getURI(), element.getLineNumber(), ple.getMessage()); Logging.errorPrint(message); setChanged(); } } } completeObject(context, entry.getSource(), object); } catch (PersistenceLayerException ple) { String message = LanguageBundle.getFormattedString( "Errors.LstFileLoader.ModParseError", //$NON-NLS-1$ entry.getSource().getURI(), entry.getLineNumber(), ple .getMessage()); Logging.errorPrint(message); setChanged(); } } /** * This method will process the lines containing a .COPY directive * @throws PersistenceLayerException */ private void processCopies(LoadContext context) throws PersistenceLayerException { for (ModEntry me : copyLineList) { context.setSourceURI(me.source.getURI()); performCopy(context, me); } copyLineList.clear(); } /** * This method will process the lines containing a .FORGET directive * @param context TODO */ private void processForgets(LoadContext context) { for (String forgetKey : forgetLineList) { forgetKey = forgetKey.substring(0, forgetKey.indexOf(FORGET_SUFFIX)); if (excludedObjects.contains(forgetKey)) { continue; } // Commented out so that deprcated method no longer used // performForget(forgetName); T objToForget = getObjectKeyed(context, forgetKey); if (objToForget != null) { performForget(context, objToForget); } } forgetLineList.clear(); } /** * This method will process the lines containing a .MOD directive */ private void processMods(LoadContext context) { for (List<ModEntry> modEntry : modEntryList) { performMod(context, modEntry); } modEntryList.clear(); } /** * This class is an entry mapping a mod to its source. * Once created, instances of this class are immutable. */ public static class ModEntry { private CampaignSourceEntry source = null; private String lstLine = null; private int lineNumber = 0; /** * ModEntry constructor. * @param aSource CampaignSourceEntry containing the MOD line * [must not be null] * @param aLstLine LST syntax modification * [must not be null] * @param aLineNumber * * @throws IllegalArgumentException if aSource or aLstLine is null. */ public ModEntry(final CampaignSourceEntry aSource, final String aLstLine, final int aLineNumber) { super(); // These are programming errors so the msgs don't need to be // internationalized. if (aSource == null) { throw new IllegalArgumentException("source must not be null"); //$NON-NLS-1$ } if (aLstLine == null) { throw new IllegalArgumentException("lstLine must not be null"); //$NON-NLS-1$ } this.source = aSource; this.lstLine = aLstLine; this.lineNumber = aLineNumber; } /** * This method gets the LST formatted source line for the .MOD * @return String in LST format, unmodified from the source file */ public String getLstLine() { return lstLine; } /** * This method gets the source of the .MOD operation * @return CampaignSourceEntry indicating where the .MOD came from */ public CampaignSourceEntry getSource() { return source; } /** * * @return The line number of the original file for this MOD entry */ public int getLineNumber() { return lineNumber; } } }