/* * Copyright 2014 (C) Tom Parker <thpr@users.sourceforge.net> * Derived from AbstractCountCommand.java * Copyright 2013 (C) James Dempsey <jdempsey@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 11/08/2013 * * $Id: AbstractCountCommand.java 22768 2014-01-04 10:35:48Z zaister $ */ package pcgen.util; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import pcgen.base.lang.UnreachableError; import pcgen.base.util.CaseInsensitiveMap; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.Category; import pcgen.cdom.content.CNAbility; import pcgen.cdom.enumeration.AspectName; import pcgen.cdom.enumeration.ListKey; import pcgen.cdom.enumeration.MapKey; import pcgen.cdom.enumeration.Nature; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.enumeration.SkillFilter; import pcgen.cdom.enumeration.Type; import pcgen.core.Ability; import pcgen.core.AbilityUtilities; import pcgen.core.ChronicleEntry; import pcgen.core.Domain; import pcgen.core.Equipment; import pcgen.core.Language; import pcgen.core.PCClass; import pcgen.core.PlayerCharacter; import pcgen.core.Skill; import pcgen.core.display.SkillDisplay; import pcgen.system.PCGenSettings; import pcgen.util.AbstractCountCommand.JepAbilityCountEnum; import pcgen.util.AbstractCountCommand.JepEquipmentCountEnum; import pcgen.util.enumeration.View; import pcgen.util.enumeration.Visibility; import org.nfunk.jep.ParseException; public abstract class JepCountType { /** * This Map contains the mappings from Strings to the Type Safe Constant */ private static CaseInsensitiveMap<JepCountType> typeMap = null; public static final JepCountType ABILITIESDISTINCT = new JepCountAbilities() { @Override protected Double countData(Collection<? extends CNAbility> filtered, PlayerCharacter pc) { if (assocList.isEmpty()) { return (double) filtered.size(); } double accum = 0; for (final CNAbility ab : filtered) { for (String assoc : pc.getAssociationList(ab)) { if (assocList.contains(assoc)) { accum++; } } } return accum; } }; public static final JepCountType ABILITIES = new JepCountAbilities() { @Override protected Double countData(Collection<? extends CNAbility> filtered, PlayerCharacter pc) { double accum = 0; for (final CNAbility ab : filtered) { if (assocList.isEmpty()) { final double ac = pc.getSelectCorrectedAssociationCount(ab); accum += 1.01 >= ac ? 1 : ac; } else { for (String assoc : pc.getAssociationList(ab)) { if (assocList.contains(assoc)) { accum++; } } } } return accum; } }; public static final JepCountType CAMPAIGNHISTORY = new JepCountFilterable<ChronicleEntry>() { @Override protected Collection<ChronicleEntry> getData( final PlayerCharacter pc) { return pc.getDisplay().getChronicleEntries(); } @Override public Double count(PlayerCharacter pc, Object[] params) throws ParseException { final Object[] par = params.length > 0 ? params : new String[]{"EXPORT=YES"}; return super.count(pc, par); } @Override protected Set<ChronicleEntry> filterSetP(final String c, Collection<ChronicleEntry> coll) throws ParseException { final String[] keyValue = c.split("="); if (!"EXPORT".equalsIgnoreCase(keyValue[0])) { throw new ParseException( "Bad parameter to count(\"CAMPAIGNHISTORY\" ... )" + c); } if (!"NO".equalsIgnoreCase(keyValue[1]) && !"YES".equalsIgnoreCase(keyValue[1])) { throw new ParseException( "Bad EXPORT value to count(\"CAMPAIGNHISTORY\" ... )" + c); } boolean wantExport = "YES".equalsIgnoreCase(keyValue[1]); final Set<ChronicleEntry> cs = new HashSet<>(); for (ChronicleEntry ce : coll) { if (ce.isOutputEntry() == wantExport) { cs.add(ce); } } return cs; } }; public static final JepCountType CLASSES = new JepCountCDOMObject<PCClass>() { @Override protected Collection<PCClass> getData(final PlayerCharacter pc) { return pc.getDisplay().getClassSet(); } }; public static final JepCountType DOMAINS = new JepCountCDOMObject<Domain>() { @Override protected Collection<Domain> getData(final PlayerCharacter pc) { return pc.getDisplay().getDomainSet(); } }; public static final JepCountType EQUIPMENT = new JepCountCDOMObject<Equipment>() { @Override protected Collection<Equipment> getData(final PlayerCharacter pc) { return pc.getEquipmentListInOutputOrder(); } @Override protected Set<? extends Equipment> filterSetP(final String c, Collection<Equipment> coll) throws ParseException { final String[] keyValue = c.split("="); final JepEquipmentCountEnum en; try { en = JepEquipmentCountEnum.valueOf(keyValue[0]); } catch (IllegalArgumentException ex) { Logging.errorPrint("Bad parameter to count(\"Equipment\"), " + c); return new HashSet<>(); } final Set<Equipment> cs = new HashSet<>(coll); final Iterator<? extends Equipment> it = cs.iterator(); switch (en) { case TYPE: filterPObjectByType(it, keyValue[1]); break; case WIELDCATEGORY: while (it.hasNext()) { final Equipment e = it.next(); if (!e.getWieldName().equalsIgnoreCase(keyValue[1])) { it.remove(); } } break; // TODO have no idea how to get a suitable list of equipment // and test for this. case LOCATION: if ("CARRIED".equalsIgnoreCase(keyValue[1]) || "Equipped".equalsIgnoreCase(keyValue[1])) { // while (it.hasNext()) // { // Equipment e = (Equipment) it.next(); // if (! e.getParent().equalsIgnoreCase(keyValue[1])); // { // it.remove(); // } // } } break; case LOC: break; case TYP: break; case WDC: break; } return cs; } }; public static final JepCountType FOLLOWERS = new JepCountType() { @Override public Number count(PlayerCharacter pc, Object[] params) { //TODO what if params is not empty?? return pc.getDisplay().getFollowerList().size(); } }; public static final JepCountType LANGUAGES = new JepCountCDOMObject<Language>() { @Override protected Collection<Language> getData(final PlayerCharacter pc) { return pc.getDisplay().getLanguageSet(); } }; public static final JepCountType RACESUBTYPE = new JepCountType() { @Override public Number count(PlayerCharacter pc, Object[] params) throws ParseException { return pc.getDisplay().getRacialSubTypeCount(); } }; public static final JepCountType SKILLS = new JepCountCDOMObject<Skill>() { @Override protected Collection<Skill> getData(PlayerCharacter pc) { return pc.getDisplay().getSkillSet(); } }; public static final JepCountType SKILLSIT = new JepCountSkillSit(); public static final JepCountType SPELLBOOKS = new JepCountType() { @Override public Number count(PlayerCharacter pc, Object[] params) { //TODO what if params is not empty?? return pc.getDisplay().getSpellBookCount(); } }; public abstract Number count(PlayerCharacter pc, Object[] params) throws ParseException; private static final class AspectFilter implements ObjectFilter<CNAbility> { private final String[] keyValue; private AspectFilter(String[] keyValue) { this.keyValue = keyValue; } public boolean accept(CNAbility o) { return o.getAbility().get(MapKey.ASPECT, AspectName.getConstant(keyValue[1])) != null; } } private static final class VisibilityFilter implements ObjectFilter<CNAbility> { private final Visibility vi; private VisibilityFilter(Visibility vi) { this.vi = vi; } public boolean accept(CNAbility o) { return o.getAbility().getSafe(ObjectKey.VISIBILITY).equals(vi); } } private static final class TypeFilter implements ObjectFilter<CNAbility> { String type; public TypeFilter(String typ) { type = typ; } public boolean accept(CNAbility o) { //isType already accounts for A.B.C, so we don't have to do that here return o.getAbility().isType(type); } } private static final class TypeExclusionFilter implements ObjectFilter<CNAbility> { String type; public TypeExclusionFilter(String typ) { type = typ; } public boolean accept(CNAbility o) { //Since this is exclude on "any" we have to expand this out StringTokenizer tok = new StringTokenizer(type, "."); Ability a = o.getAbility(); while (tok.hasMoreTokens()) { if (a.containsInList(ListKey.TYPE, Type.getConstant(tok.nextToken()))) { return false; } } return true; } } private static final class NatureFilter implements ObjectFilter<CNAbility> { Nature nature; public NatureFilter(Nature n) { nature = n; } public boolean accept(CNAbility o) { return o.getNature().equals(nature); } } private static final class KeyNameFilter implements ObjectFilter<CNAbility> { private final String name; private final List<String> assocList; private KeyNameFilter(String keyValue, List<String> list) { this.name = keyValue; assocList = list; } public boolean accept(CNAbility o) { List<String> assocs = new ArrayList<>(); String undec = AbilityUtilities.getUndecoratedName(name, assocs); Ability ab = o.getAbility(); String keyName = ab.getKeyName(); if (keyName.equalsIgnoreCase(undec)) { assocList.addAll(assocs); return true; } return keyName.equalsIgnoreCase(name); } } private static final class DisplayNameFilter implements ObjectFilter<CNAbility> { private final String name; private DisplayNameFilter(String keyValue) { this.name = keyValue; } public boolean accept(CNAbility o) { return o.getAbility().getDisplayName().equalsIgnoreCase(name); } } private static final class CategoryFilter implements ObjectFilter<CNAbility> { private final String cat; private CategoryFilter(String cat) { this.cat = cat; } public boolean accept(CNAbility o) { Category<Ability> parentCategory = o.getAbilityCategory().getParentCategory(); return parentCategory.getKeyName().equalsIgnoreCase(cat); } } public static abstract class JepCountCDOMObject<T extends CDOMObject> extends JepCountFilterable<T> { @Override public Double count(PlayerCharacter pc, Object[] params) throws ParseException { return super.count(pc, validateParams(params)); } // By adding this it means that we can call count with just the object to be // counted and get a count of all e.g. count("ABILITIES") will return a // count of all abilities with no filtering at all. protected Object[] validateParams(final Object[] params) throws ParseException { Object[] p = new Object[1]; if (1 > params.length) { p[0] = "TYPE=ALL"; } else { p = params; } return p; } @Override protected Set<? extends T> filterSetP(final String c, Collection<T> coll) throws ParseException { final String[] keyValue = c.split("="); if (!"TYPE".equalsIgnoreCase(keyValue[0])) { throw new ParseException( "Bad parameter to count(\"CLASSES\" ... )" + c); } final Set<T> cs = new HashSet<>(coll); final Iterator<? extends T> it = cs.iterator(); filterPObjectByType(it, keyValue[1]); return cs; } protected void filterPObjectByType(final Iterator<? extends T> it, final String tString) { // If we want all then we don't need to filter. if (!"ALL".equalsIgnoreCase(tString)) { // Make a List of all the types that each PObject should match final Collection<String> typeList = new ArrayList<>(); Collections.addAll(typeList, tString.split("\\.")); // These nested loops remove all PObjects from the collection being // iterated that do not match all of the types in typeList while (it.hasNext()) { final T pObj = it.next(); for (final String type : typeList) { if (!pObj.isType(type)) { it.remove(); break; } } } } } } public static abstract class JepCountFilterable<T> extends JepCountType { protected abstract Collection<T> getData(final PlayerCharacter pc); protected static ParameterTree convertParams(final Object[] params) { ParameterTree pt = null; for (final Object param : params) { try { if (pt == null) { pt = ParameterTree.makeTree((String) param); } else { final ParameterTree npt = ParameterTree.makeTree(ParameterTree.andString); npt.setLeftTree(pt); pt = npt; final ParameterTree npt1 = ParameterTree.makeTree((String) param); pt.setRightTree(npt1); } } catch (ParseException pe) { Logging.errorPrint(MessageFormat.format( "Malformed parameter to count {0}", param), pe); } } return pt; } protected Collection<? extends T> doFilterP(final ParameterTree pt, Collection<T> coll) throws ParseException { final String c = pt.getContents(); if (c.equalsIgnoreCase(ParameterTree.orString) || c.equalsIgnoreCase(ParameterTree.andString)) { final Set<T> a = new HashSet<>(doFilterP(pt.getLeftTree(), coll)); final Collection<? extends T> b = doFilterP(pt.getRightTree(), coll); if (c.equalsIgnoreCase(ParameterTree.orString)) { a.addAll(b); } else { a.retainAll(b); } return a; } return filterSetP(c, coll); } @Override public Double count(PlayerCharacter pc, Object[] params) throws ParseException { final ParameterTree pt = convertParams(params); Collection<T> data = getData(pc); Collection<? extends T> results; if (pt == null) { results = data; } else { results = doFilterP(pt, data); } return countData(results, pc); } protected Double countData(final Collection<? extends T> filtered, PlayerCharacter pc) { return (double) filtered.size(); } protected abstract Collection<? extends T> filterSetP(String c, Collection<T> coll) throws ParseException; } public static abstract class JepCountAbilities extends JepCountFilterable<CNAbility> { protected final List<String> assocList = new ArrayList<>(); @Override protected Collection<CNAbility> getData(final PlayerCharacter pc) { assocList.clear(); return pc.getCNAbilities(); } @Override protected Collection<? extends CNAbility> filterSetP(final String c, Collection<CNAbility> coll) { final String[] keyValue = c.split("="); final JepAbilityCountEnum en; try { en = JepAbilityCountEnum.valueOf(keyValue[0]); } catch (IllegalArgumentException ex) { Logging.errorPrint("Bad parameter to count(\"Ability\"), " + c); return new HashSet<>(); } ObjectFilter<CNAbility> filter = null; switch (en) { case CATEGORY: case CAT: filter = new CategoryFilter(keyValue[1]); break; case NAME: case NAM: //TODO need to initialize assocFilter :/ filter = new DisplayNameFilter(keyValue[1]); break; case KEY: filter = new KeyNameFilter(keyValue[1], assocList); break; case NATURE: case NAT: try { Nature n = Nature.valueOf(keyValue[1]); if (!n.equals(Nature.ANY)) { filter = new NatureFilter(n); } } catch (IllegalArgumentException ex) { Logging .errorPrint("Bad parameter to count(\"Ability\"), no such NATURE " + c); } break; case TYPE: case TYP: filter = new TypeFilter(keyValue[1]); break; case EXCLUDETYPE: filter = new TypeExclusionFilter(keyValue[1]); break; case VISIBILITY: case VIS: try { final Visibility vi = Visibility.valueOf(keyValue[1]); filter = new VisibilityFilter(vi); } catch (IllegalArgumentException ex) { Logging .errorPrint("Bad parameter to count(\"Ability\"), no such Visibility " + keyValue[1]); } break; case ASPECT: filter = new AspectFilter(keyValue); break; } List<CNAbility> ret = new ArrayList<>(coll); if (filter != null) { for (Iterator<CNAbility> it = ret.iterator(); it.hasNext();) { CNAbility cna = it.next(); if (!filter.accept(cna)) { it.remove(); } } } return ret; } } public interface ObjectFilter<T> { public boolean accept(T o); } private static void buildMap() { typeMap = new CaseInsensitiveMap<>(); Field[] fields = JepCountType.class.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { int mod = fields[i].getModifiers(); if (Modifier.isStatic(mod) && Modifier.isFinal(mod) && Modifier.isPublic(mod)) { try { Object obj = fields[i].get(null); if (obj instanceof JepCountType) { typeMap.put(fields[i].getName(), (JepCountType) obj); } } catch (IllegalArgumentException | IllegalAccessException e) { throw new UnreachableError(e); } } } } /** * Returns the constant for the given String (the search for the constant is * case insensitive). * * @param name * The name of the constant to be returned * @return The Constant for the given name */ public static synchronized JepCountType valueOf(String name) { if (typeMap == null) { buildMap(); } return typeMap.get(name); } /** * Returns a Collection of all of the Constants in this Class. * * This collection maintains a reference to the Constants in this Class, so * if a new Constant is created, the Collection returned by this method will * be modified. (Beware of ConcurrentModificationExceptions) * * @return a Collection of all of the Constants in this Class. */ public static synchronized Collection<JepCountType> getAllConstants() { if (typeMap == null) { buildMap(); } return Collections.unmodifiableCollection(typeMap.values()); } private static class JepCountSkillSit extends JepCountType { @Override public Number count(PlayerCharacter pc, Object[] params) throws ParseException { SkillFilter sf = null; View v = View.ALL; if (params.length == 0) { return processCount(pc, sf, v); } if (params.length > 2) { Logging .errorPrint("count(\"SKILLSIT\") allows up to 2 parameters"); } int nextparameter = 0; //There is at least one parameter, but we don't know what kind String filtername = params[nextparameter++].toString(); //If the filter is a SkillFilter, use it sf = SkillFilter.getByToken(filtername); if (sf != null) { if (params.length == 1) { //If it was just a skill filter, we're done return processCount(pc, sf, v); } else { //else we fall through to a VIEW using the next parameter filtername = params[nextparameter++].toString(); } } //Now must start with VIEW= if (filtername.startsWith("VIEW=")) { v = View.getViewFromName(filtername.substring(5)); if (v == null) { Logging .errorPrint("count(\"SKILLSIT\") found View it does not understand: " + filtername + " Legal values are: " + Arrays.asList(View.values())); } } else { Logging .errorPrint("count(\"SKILLSIT\") found parameter (Skill Filter?) " + "it does not understand: " + filtername); } while (nextparameter != params.length) { Logging.errorPrint("count(\"SKILLSIT\") found parameter " + "it did not expect (out of order?): '" + params[nextparameter++] + "'. Parameter was ignored."); } return processCount(pc, sf, v); } private Number processCount(PlayerCharacter pc, SkillFilter sf, View v) { if (sf == null) { sf = getDefaultSkillFilter(pc); } int count = 0; final List<Skill> skills = SkillDisplay.getSkillListInOutputOrder(pc, pc.getDisplay() .getPartialSkillList(v)); for (Skill sk : skills) { if (pc.includeSkill(sk, sf) && sk.qualifies(pc, null)) { count++; //For the skill for (String situation : sk .getUniqueListFor(ListKey.SITUATION)) { double bonus = pc.getTotalBonusTo("SITUATION", sk.getKeyName() + "=" + situation); if (bonus > 0.01 || bonus < -0.01) { count++; } } } } return (double) count; } private SkillFilter getDefaultSkillFilter(PlayerCharacter pc) { if (pc == null) { return SkillFilter.getByValue(PCGenSettings.OPTIONS_CONTEXT.initInt( PCGenSettings.OPTION_SKILL_FILTER, SkillFilter.Usable.getValue())); } else { return pc.getSkillFilter(); } } } }