/*
* CampaignSourceEntry.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:29 PM
*
* Current Ver: $Revision$
* Last Editor: $Author$
* Last Edited: $Date$
*
*/
package pcgen.persistence.lst;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import pcgen.base.lang.StringUtil;
import pcgen.base.lang.UnreachableError;
import pcgen.base.util.HashMapToList;
import pcgen.base.util.MapToList;
import pcgen.cdom.base.Constants;
import pcgen.core.Campaign;
import pcgen.core.prereq.Prerequisite;
import pcgen.core.utils.CoreUtility;
import pcgen.persistence.PersistenceLayerException;
import pcgen.persistence.lst.output.prereq.PrerequisiteWriter;
import pcgen.persistence.lst.prereq.PreParserFactory;
import pcgen.system.ConfigurationSettings;
import pcgen.system.PCGenSettings;
import pcgen.util.Logging;
/**
* This class is used to match a source file to the campaign that
* loaded it.
*/
public class CampaignSourceEntry implements SourceEntry
{
public static final URI FAILED_URI;
static
{
try
{
FAILED_URI = new URI("file:/FAIL");
}
catch (URISyntaxException e)
{
throw new UnreachableError(e);
}
}
private Campaign campaign = null;
private List<String> excludeItems = new ArrayList<String>();
private List<String> includeItems = new ArrayList<String>();
private List<Prerequisite> prerequisites = new ArrayList<Prerequisite>();
private URI uri = null;
private URI writeURI = null;
private URIFactory uriFac = null;
private String stringForm = null;
/**
* CampaignSourceEntry constructor.
*
* @param campaign Campaign that referenced the provided file.
* Must not be null.
* @param lstLoc URL path to an LST source file
* Must not be null.
*/
public CampaignSourceEntry(Campaign campaign, URI lstLoc)
{
super();
if (campaign == null)
{
throw new IllegalArgumentException("campaign can't be null");
}
if (lstLoc == null)
{
throw new IllegalArgumentException("lstLoc can't be null");
}
this.campaign = campaign;
this.uri = lstLoc;
}
public CampaignSourceEntry(Campaign campaign, URIFactory fac)
{
super();
if (campaign == null)
{
throw new IllegalArgumentException("campaign can't be null");
}
if (fac == null)
{
throw new IllegalArgumentException("URI Factory can't be null");
}
this.campaign = campaign;
this.uriFac = fac;
}
public static class URIFactory
{
private final URI u;
private final String s;
public URIFactory(URI source, String value)
{
if (source == null)
{
throw new IllegalArgumentException("URI cannot be null");
}
if (value == null || value.length() == 0)
{
throw new IllegalArgumentException("URI cannot be null");
}
u = source;
s = value;
}
public URI getURI()
{
URI uri = getNonNormalizedPathURI(u, s);
return uri.normalize();
}
@Override
public int hashCode()
{
return s.hashCode();
}
@Override
public boolean equals(Object o)
{
if (o instanceof URIFactory)
{
URIFactory other = (URIFactory) o;
return s.equals(other.s) && u.equals(other.u);
}
return false;
}
}
/**
* This method gets the Campaign that was the source of the
* file. (I.e. the reason it was loaded)
* @return Campaign that requested the file be loaded
*/
@Override
public Campaign getCampaign()
{
return campaign;
}
/**
* This method gets a list of the items contained in the given source
* file to exclude from getting saved in memory. All other objects
* in the file are to be included.
* @return List of String names of objects to exclude
*/
@Override
public List<String> getExcludeItems()
{
return excludeItems;
}
/**
* This method gets the file/path of the LST file.
* @return String url-formatted path to the LST file
*/
@Override
public URI getURI()
{
if (uri == null)
{
uri = uriFac.getURI();
}
return uri;
}
/**
* This method gets a list of the items containined in the given source
* file to include in getting saved in memory. All other objects
* in the file are to be excluded.
* @return List of String names of objects to include
*/
@Override
public List<String> getIncludeItems()
{
return includeItems;
}
/**
* @param arg0
* @return true if equals
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object arg0)
{
if (arg0 == this)
{
return true;
}
if (!(arg0 instanceof CampaignSourceEntry))
{
return false;
}
CampaignSourceEntry other = (CampaignSourceEntry) arg0;
if (this.uriFac == null)
{
if (other.uriFac != null || !getURI().equals(other.getURI()))
{
return false;
}
}
else
{
if (!uriFac.equals(other.uriFac))
{
return false;
}
}
return excludeItems.equals(other.excludeItems)
&& includeItems.equals(other.includeItems);
}
/**
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode()
{
return this.getURIIdentifier().hashCode();
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
if (stringForm == null)
{
StringBuilder sBuff = new StringBuilder();
sBuff.append("Campaign: ");
sBuff.append(campaign.getDisplayName());
sBuff.append("; SourceFile: ");
sBuff.append(getURI());
stringForm = sBuff.toString();
}
return stringForm;
}
/**
* This method converts the provided filePath to either a URL
* or absolute path as appropriate.
*
* @param pccPath URL where the Campaign that contained the source was at
* @param basePath String path that is to be converted
* @return String containing the converted absolute path or URL
* (as appropriate)
*/
private static URI getNonNormalizedPathURI(URI pccPath, String basePath)
{
if (basePath.length() <= 0)
{
Logging.errorPrint("Empty Path to LST file in " + pccPath);
return FAILED_URI;
}
/*
* Figure out where the PCC file came from that we're processing, so
* that we can prepend its path onto any LST file references (or PCC
* refs, for that matter) that are relative. If the source line in
* question already has path info, then don't bother
*/
if (basePath.charAt(0) == '@')
{
String pathNoLeader =
trimLeadingFileSeparator(basePath.substring(1));
String path = CoreUtility.fixFilenamePath(pathNoLeader);
return new File(ConfigurationSettings.getPccFilesDir(), path)
.toURI();
}
else if (basePath.charAt(0) == '&')
{
String pathNoLeader =
trimLeadingFileSeparator(basePath.substring(1));
String path = CoreUtility.fixFilenamePath(pathNoLeader);
return new File(PCGenSettings.getVendorDataDir(), path)
.toURI();
}
else if (basePath.charAt(0) == '$')
{
String pathNoLeader =
trimLeadingFileSeparator(basePath.substring(1));
String path = CoreUtility.fixFilenamePath(pathNoLeader);
return new File(PCGenSettings.getHomebrewDataDir(), path)
.toURI();
}
else if (basePath.charAt(0) == '*')
{
String pathNoLeader =
trimLeadingFileSeparator(basePath.substring(1));
String path = CoreUtility.fixFilenamePath(pathNoLeader);
File pccFile =
new File(PCGenSettings.getHomebrewDataDir(), path);
if (pccFile.exists())
{
return pccFile.toURI();
}
pccFile =
new File(PCGenSettings.getVendorDataDir(), path);
if (pccFile.exists())
{
return pccFile.toURI();
}
return new File(ConfigurationSettings.getPccFilesDir(), path)
.toURI();
}
/*
* If the line doesn't use "@", "&", or "$" then it's a relative path
*
* 1) If the path starts with '/data', assume it means the PCGen
* data dir 2) Otherwise, assume that the path is relative to the
* current PCC file URL
*/
String pathNoLeader = trimLeadingFileSeparator(basePath);
if (pathNoLeader.startsWith("data"))
{
// substring 5 to eliminate the separator after data
String path =
CoreUtility.fixFilenamePath(pathNoLeader.substring(5));
return new File(ConfigurationSettings.getPccFilesDir(), path)
.toURI();
}
else
{
if (basePath.indexOf(':') > 0)
{
try
{
// if it's a URL, then we are all done, just return a URI
URL url = new URL(basePath);
return new URI(url.getProtocol(), url.getHost(), url
.getPath(), null);
}
catch (URISyntaxException e)
{
//Something broke, so wasn't a URL
}
catch (MalformedURLException e)
{
//Protocol was unknown, so wasn't a URL
}
}
String path = pccPath.getPath();
// URLs always use forward slash; take off the file name
try
{
return new URI(pccPath.getScheme(), null, (path.substring(0,
path.lastIndexOf('/') + 1) + basePath.replace('\\', '/')),
null);
}
catch (URISyntaxException e)
{
Logging.errorPrint("GPURI failed to convert "
+ path.substring(0, path.lastIndexOf('/') + 1) + basePath
+ " to a URI: " + e.getLocalizedMessage());
}
}
return FAILED_URI;
}
/**
* This method trims the leading file separator or URL separator from the
* front of a string.
*
* @param basePath String containing the base path to trim
* @return String containing the trimmed path String
*/
private static String trimLeadingFileSeparator(String basePath)
{
String pathNoLeader = basePath;
if (pathNoLeader.startsWith("/")
|| pathNoLeader.startsWith(File.separator))
{
pathNoLeader = pathNoLeader.substring(1);
}
return pathNoLeader;
}
public static CampaignSourceEntry getNewCSE(Campaign campaign2,
URI sourceUri, String value)
{
if (value == null || value.length() == 0)
{
Logging
.errorPrint("Cannot build CampaignSourceEntry for empty value in "
+ sourceUri);
return null;
}
// Check if include/exclude items were present
int pipePos = value.indexOf("|");
CampaignSourceEntry cse;
if (pipePos == -1)
{
if (value.startsWith("("))
{
Logging.errorPrint("Invalid Campaign File, cannot start with (:" + value);
return null;
}
cse =
new CampaignSourceEntry(campaign2, new URIFactory(
sourceUri, value));
}
else
{
cse =
new CampaignSourceEntry(campaign2, new URIFactory(
sourceUri, value.substring(0, pipePos)));
// Get the include/exclude item string
String inExString = value.substring(pipePos + 1);
List<String> tagList = parseSuffix(inExString, sourceUri, value);
for (String tagString : tagList)
{
// Check for surrounding parens
if (tagString.startsWith("(("))
{
Logging
.errorPrint("Found Suffix in Campaign Source with multiple parenthesis: "
+ "Single set of parens required around INCLUDE/EXCLUDE");
Logging.errorPrint("Found: '" + tagString
+ "' in " + value);
return null;
}
// Update the include or exclude items list, as appropriate
if (tagString.startsWith("(INCLUDE:"))
{
// assume matching parens
tagString = inExString.substring(1, tagString.length() - 1);
List<String> splitIncExc = cse.splitInExString(tagString);
if (splitIncExc == null)
{
//Error
return null;
}
cse.includeItems = splitIncExc;
}
else if (tagString.startsWith("(EXCLUDE:"))
{
// assume matching parens
tagString = inExString.substring(1, tagString.length() - 1);
List<String> splitIncExc = cse.splitInExString(tagString);
if (splitIncExc == null)
{
//Error
return null;
}
cse.excludeItems = splitIncExc;
}
else if (PreParserFactory.isPreReqString(tagString))
{
Prerequisite prereq;
try
{
prereq = PreParserFactory.getInstance().parse(tagString);
}
catch (PersistenceLayerException e)
{
Logging.errorPrint(
"Error Initializing PreParserFactory.", e);
return null;
}
if (prereq == null)
{
Logging
.errorPrint("Found invalid prerequisite in Campaign Source: '"
+ tagString + "' in " + value);
return null;
}
cse.prerequisites.add(prereq);
}
else
{
Logging.errorPrint("Invalid Suffix (must have "
+ "'(INCLUDE' '(EXCLUDE' or a PRExxx immediately "
+ "following the pipe (no spaces). Found: '" + inExString
+ "' on Campaign Source: '" + value + "' in " + sourceUri);
return null;
}
}
validatePrereqs(cse.getPrerequisites(), sourceUri);
}
return cse;
}
/**
* Convert a string occurring after the first | into a list of tokens. We
* expect INCLUDE or EXCLUSE in brackets (as these can contain |)
* and PREreqs.
*
* @param suffix The string to be parsed, should only be the suffix
* @param sourceUri The source we can use to report errors against.
* @param value The full value we can use to report errors against.
* @return A list of the discrete tags that were specified, null if there
* was an error reported to the log.
*/
static List<String> parseSuffix(String suffix, URI sourceUri,
String value)
{
List<String> tagList = new ArrayList<String>();
String currentTag = "";
int bracketLevel = 0;
StringTokenizer tokenizer =
new StringTokenizer(suffix, "|()", true);
while (tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken();
if (token.equals("("))
{
currentTag += token;
bracketLevel++;
}
else if (token.equals(")"))
{
if (bracketLevel > 0)
{
bracketLevel--;
}
currentTag += token;
if (bracketLevel == 0)
{
tagList.add(currentTag);
currentTag = "";
}
}
else if (token.equals("|"))
{
if (bracketLevel > 0)
{
currentTag += token;
}
}
else if (bracketLevel > 0)
{
currentTag += token;
}
else
{
tagList.add(token);
}
}
// Check for a bracket mismatch
if (bracketLevel > 0)
{
Logging
.errorPrint("Suffix in Campaign Source with missing closing parenthesis, Found: '"
+ suffix
+ "' on Campaign Source: '"
+ value
+ "' in "
+ sourceUri);
return null;
}
return tagList;
}
/**
* Check that all prerequisites specified in the PCC file are
* supported. Any unsupported prereqs will be reported as LST
* errors. This is a recursive function allowing it to
* check nested prereqs.
*
* @param prereqList The prerequisites to be checked.
*/
private static void validatePrereqs(List<Prerequisite> prereqList, URI sourceUri)
{
if (prereqList == null || prereqList.isEmpty())
{
return;
}
for (Prerequisite prereq : prereqList)
{
if (prereq.isCharacterRequired())
{
final PrerequisiteWriter prereqWriter =
new PrerequisiteWriter();
ArrayList<Prerequisite> displayList = new ArrayList<Prerequisite>();
displayList.add(prereq);
String lstString =
prereqWriter.getPrerequisiteString(displayList,
Constants.TAB);
Logging.log(Logging.LST_ERROR, "Prereq '" + prereq.getKind()
+ "' is not supported in PCC files. Prereq was '" + lstString
+ "' in " + sourceUri + ". Prereq will be ignored.");
}
else
{
validatePrereqs(prereq.getPrerequisites(), sourceUri);
}
}
}
public URI getWriteURI()
{
return writeURI;
}
public void setWriteURI(URI writeURI)
{
this.writeURI = writeURI;
}
/**
* Split an include or exclude string accounting for the possible presence
* of a leading category.
* @param inExString The string to be split
* @return A list of keys, optionally with leading category keys
*/
private List<String> splitInExString(String inExString)
{
boolean hasCategory = false;
boolean hasKeyOnly = false;
List<String> catKeyList = new ArrayList<String>();
String target = inExString.substring(8);
if (target == null || target.length() == 0)
{
Logging.errorPrint("Must Specify Items after :");
return null;
}
List<String> keyList = CoreUtility.split(target, '|');
for (String key : keyList)
{
if (key.startsWith("CATEGORY="))
{
hasCategory = true;
List<String> abilityKeyList =
CoreUtility.split(key.substring(9), ',');
String category = abilityKeyList.get(0);
abilityKeyList.remove(0);
for (String string : abilityKeyList)
{
catKeyList.add(category + ',' + string);
}
}
else
{
hasKeyOnly = true;
catKeyList.add(key);
}
}
if (hasKeyOnly && hasCategory)
{
Logging.log(Logging.LST_ERROR, "Invalid "
+ inExString.substring(0, 7) + " value on " + getURIIdentifier() + " in "
+ campaign.getDisplayName()
+ ". Abilities must always have categories (e.g. "
+ inExString.substring(0, 8)
+ "CATEGORY=cat1,key1,key2|CATEGORY=cat2,key1 ) and "
+ "other file types should never have categories (e.g. "
+ inExString.substring(0, 8) + "key1|key2 ).");
return null;
}
return catKeyList;
}
private String getURIIdentifier()
{
if (uriFac == null)
{
return uri.toString();
}
else
{
return uriFac.s;
}
}
public String getLSTformat()
{
StringBuilder sb = new StringBuilder();
sb.append(getURIIdentifier());
if (!includeItems.isEmpty())
{
sb.append(Constants.PIPE);
sb.append("(INCLUDE:");
sb.append(joinIncExcList(includeItems));
sb.append(')');
}
else if (!excludeItems.isEmpty())
{
sb.append(Constants.PIPE);
sb.append("(EXCLUDE:");
sb.append(joinIncExcList(excludeItems));
sb.append(')');
}
return sb.toString();
}
private StringBuilder joinIncExcList(List<String> list)
{
MapToList<String, String> map = new HashMapToList<String, String>();
for (String s : list)
{
int commaLoc = s.indexOf(',');
if (commaLoc == -1)
{
return StringUtil.joinToStringBuilder(list, Constants.PIPE);
}
else
{
map.addToListFor(s.substring(0, commaLoc), s
.substring(commaLoc + 1));
}
}
StringBuilder sb = new StringBuilder(200);
boolean needPipe = false;
for (String category : map.getKeySet())
{
if (needPipe)
{
sb.append(Constants.PIPE);
}
needPipe = true;
sb.append("CATEGORY=");
sb.append(category);
sb.append(Constants.COMMA);
sb.append(StringUtil.joinToStringBuilder(map.getListFor(category),
Constants.COMMA));
}
return sb;
}
public CampaignSourceEntry getRelatedTarget(String fileName)
{
if (uriFac == null)
{
throw new IllegalStateException(
"getRelatedTarget can only be called on a CampaignSourceEntry that uses a URI Factory");
}
return new CampaignSourceEntry(campaign, new URIFactory(uriFac.u,
fileName));
}
public List<Prerequisite> getPrerequisites()
{
return prerequisites;
}
}