package bjc.everge; import java.util.*; import java.util.function.*; import java.util.regex.*; import bjc.everge.ControlledString.*; /** * String pairs for replacements. * * @author Ben Culkin */ public class ReplPair implements Comparable, UnaryOperator { // Line number we read this pair from private int lno; // Stage this pair is in private int stage; // Status of this pair with regards to doing staging stuff private StageStatus stat = StageStatus.BOTH; /** * The priority for this replacement. */ public int priority; /** * The name of this replacement. * * Defaults to the 'find' string. */ public String name; /** * The guard for this replacement. * * The guard of the replacement is a regex that has to match before the pair * will be considered. Defaults to being blank. */ public String guard; /** * The string to look for. */ public String find; /** * The string to replace it with. */ public String replace; /** * Create a new blank replacement pair. */ public ReplPair() { this("", "", 1, null); } /** * Create a new replacement pair with a priority of 1. * * @param f * The string to find. * @param r * The string to replace. */ public ReplPair(String f, String r) { this(f, r, 1); } /** * Create a new named replacement pair with a priority of 1. * * @param f * The string to find. * @param r * The string to replace. * @param n * The name of the replacement pair. */ public ReplPair(String f, String r, String n) { this(f, r, 1, n); } /** * Create a new replacement pair with a set priority. * * @param f * The string to find. * @param r * The string to replace. * @param p * The priority for the replacement. */ public ReplPair(String f, String r, int p) { this(f, r, p, f); } /** * Create a new replacement pair with a set priority and name. * * @param f * The string to find. * @param r * The string to replace. * @param n * The name of the replacement pair. * @param p * The priority for the replacement. */ public ReplPair(String f, String r, int p, String n) { find = f; replace = r; name = n; priority = p; } /** * Read a list of replacement pairs from an input source. * * @param scn * The source to read the replacements from. * @return The list of replacements. */ public static List readList(Scanner scn) { List lst = new ArrayList<>(); return readList(lst, scn); } /** * Read a list of replacement pairs from an input source, adding them to an * existing list. * * @param detals * The list to add the replacements to. * @param scn * The source to read the replacements from. * @return The list of replacements. */ public static List readList(List detals, Scanner scn) { List errList = new ArrayList<>(); List rplPar = readList(detals, scn, errList); if (errList.size() != 0) { throw new BadReplParse("", errList); } return rplPar; } /** * Read a list of replacement pairs from an input source, adding them to an * existing list. * * @param detals * The list to add the replacements to. * @param scn * The source to read the replacements from. * @param errs * The list to stick errors in. * @return The list of replacements. */ public static List readList(List detals, Scanner scn, List errs) { return readList(detals, scn, errs, new ReplOpts()); } /** * Read a list of replacement pairs from an input source, adding them to an * existing list. * * @param detals * The list to add the replacements to. * @param scn * The source to read the replacements from. * @param errs * The list to stick errors in. * @param ropts * The options to use when reading the pairs. * @return The list of replacements. */ public static List readList(List detals, Scanner scn, List errs, ReplOpts ropts) { IntHolder lno = new IntHolder(); IntHolder pno = new IntHolder(); List> stages = new ArrayList<>(); stages.add(new ArrayList()); // For every line in the source... while (scn.hasNextLine()) { String name = scn.nextLine().trim(); lno.incr(); // If its commented or blank, skip it if (name.equals("")) continue; if (name.startsWith("#")) continue; // Global control. Process it. if (name.startsWith("|//")) { readGlobal(name, scn, errs, ropts, lno, pno); continue; } ReplPair rp = new ReplPair(); rp.priority = ropts.defPrior; rp.stat = ropts.defStatus; rp.lno = lno.get(); rp.stage = ropts.defStage; boolean isMulti = ropts.defMulti; { String tmpName = readName(name, scn, errs, rp, ropts, lno, pno); if (tmpName == null) continue; name = tmpName; } rp.find = name; if (rp.name == null) rp.name = name; // We started to process the pair, mark it as being // started pno.incr(); String body = null; // Read in the next uncommented line do { if (!scn.hasNextLine()) break; body = scn.nextLine().trim(); lno.incr(); } while (body.startsWith("#")); if (body == null) { String msg = String.format( "Ran out of input looking for replacement body for raw name '%s'", name); errs.add(new ReplError(lno, pno, msg, null)); break; } isMulti = ropts.defMulti; ControlledString cs = getControls(body, errs, ropts, lno, pno, "body"); // Body has attached controls, process them. if (cs.hasControls()) { for (Control cont : cs.controls) { switch (cont.name) { case "MULTITRUE": case "MULTIT": case "MT": isMulti = true; break; case "MULTIFALSE": case "MULTIF": case "MF": isMulti = false; break; case "MULTI": case "M": if (cont.count() != 1) { String errMsg = String.format( "Expected one multi flag (got %d)", cont.count()); errs.add(new ReplError(lno, pno, errMsg, body)); } else { isMulti = Boolean.parseBoolean(cont.get(0)); } break; default: { String errMsg = String.format("Invalid control name '%s'", cont.name); errs.add(new ReplError(lno, pno, errMsg, body)); } break; } } body = cs.strang; } if (isMulti) { String tmp = readMultiLine(body, scn, ropts, "body", lno); if (tmp == null) continue; body = tmp; } rp.replace = body; List stageList = null; if (rp.stage == 0 || stages.size() < (rp.stage - 1)) { stageList = stages.get(rp.stage); if (stageList == null) { stageList = new ArrayList<>(); stages.add(rp.stage, stageList); } } else { for (int i = stages.size(); i <= rp.stage; i++) { stages.add(new ArrayList<>()); } stageList = stages.get(rp.stage); } if (ropts.isTrace) { ropts.errStream.printf("\t[DEBUG] Stage %d: Added %s\n\t\tContents: %s\n", rp.stage, rp, stageList); } stageList.add(rp); } // Special-case one-stage processing. if (stages.size() == 1) { if (ropts.isTrace) ropts.errStream.printf("\t[DEBUG] Executing single-stage bypass\n"); for (ReplPair rp : stages.iterator().next()) { if (rp.stat == StageStatus.INTERNAL) { if (ropts.isTrace) ropts.errStream.printf("\t[DEBUG] Excluding internal RP %s\n", rp); continue; } detals.add(rp); } detals.sort(null); return detals; } // Handle stages List tmpList = new ArrayList<>(); tmpList.addAll(detals); if (ropts.isTrace) ropts.errStream.printf("\t[DEBUG] Stages: %s\n", stages); int procStg = 0; for (List stageList : stages) { procStg += 1; List curStage = new ArrayList<>(); if (ropts.isTrace) ropts.errStream.printf("\t[DEBUG] Staging stage %d of %d: %s\n", procStg, stageList.size(), stageList); for (ReplPair rp : stageList) { // Process through every pair in the previous // stages for (ReplPair curPar : tmpList) { String tmp = rp.replace.replaceAll(curPar.find, curPar.replace); if (ropts.isTrace && !rp.replace.equals(tmp)) { ropts.errStream.printf("\t[DEBUG] Staged '%s' -> '%s'\t%s\n", rp.replace, tmp, curPar); } rp.replace = tmp; } // If we're external; add straight to the output if (rp.stat == StageStatus.EXTERNAL) { if (ropts.isTrace) { ropts.errStream.printf( "\t[DEBUG] Skipped external for staging: %s\n", rp); } detals.add(rp); } else { if (ropts.isTrace) { ropts.errStream.printf( "\t[DEBUG] Added to stage %d: %s\n\t\tContents: %s\n", procStg, rp, curStage); } curStage.add(rp); } } tmpList.addAll(curStage); tmpList.sort(null); } // Copy over to output, excluding internals for (ReplPair rp : tmpList) { if (rp.stat == StageStatus.INTERNAL) { if (ropts.isTrace) ropts.errStream.printf("\t[DEBUG] Excluded internal: %s\n", rp); continue; } detals.add(rp); } detals.sort(null); if (ropts.isTrace) { ropts.errStream.printf("\t[DEBUG] Final output: %s\n", detals); } return detals; } private static String readMultiLine(String lead, Scanner src, ReplOpts ropts, String typ, IntHolder lno) { String tmp = lead; if (ropts.isTrace && tmp.endsWith("\\")) ropts.errStream.printf("\t[TRACE] Starting multi-line parse for %s '%s'\n", typ, tmp); boolean didMulti = tmp.endsWith("\\"); while (tmp.endsWith("\\")) { boolean incNL = tmp.endsWith("|\\"); if (!src.hasNextLine()) break; String nxt = src.nextLine().trim(); lno.incr(); if (nxt.startsWith("#")) continue; String nlStr = incNL ? "\n" : ""; if (tmp.endsWith("\\")) { if (incNL) { tmp = tmp.substring(0, tmp.length() - 2); } else { tmp = tmp.substring(0, tmp.length() - 1); } } tmp = String.format("%s%s%s", tmp, nlStr, nxt); } if (ropts.isTrace && didMulti) ropts.errStream.printf("\t[TRACE] Finished multi-line parse for %s:\n%s\n.\n", typ, tmp); return tmp; } @Override public String apply(String inp) { if (guard != null) { if (!inp.matches(guard)) return inp; } // FIXME :EndingSlash Ben Culkin 5/20/20 // In the event that replace ends with a \, that throws a confusing exception String res = inp.replaceAll(find, replace); return res; } @Override public String toString() { String nameStr = ""; if (!find.equals(name)) nameStr = String.format("(%s)", name); return String.format("%ss/(%s)/(%s)/p(%d)", nameStr, find, replace, priority); } @Override public int compareTo(ReplPair rp) { if (this.priority == rp.priority) return this.lno - rp.lno; return rp.priority - this.priority; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((find == null) ? 0 : find.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + priority; result = prime * result + ((replace == null) ? 0 : replace.hashCode()); result = prime * result + stage; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ReplPair other = (ReplPair) obj; if (find == null) { if (other.find != null) return false; } else if (!find.equals(other.find)) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; if (priority != other.priority) return false; if (replace == null) { if (other.replace != null) return false; } else if (!replace.equals(other.replace)) return false; if (stage != other.stage) return false; return true; } private static String readName(String nam, Scanner scn, List errs, ReplPair rp, ReplOpts ropts, IntHolder lno, IntHolder pno) { ControlledString cs = getControls(nam, errs, ropts, lno, pno, "name"); boolean isMulti = ropts.defMulti; String name = cs.strang; if (cs.hasControls()) { for (Control cont : cs.controls) { switch (cont.name) { case "NAME": case "N": if (cont.count() != 1) { String errMsg = String.format( "One name argument was expected (got %d)", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { rp.name = cont.get(0); } break; case "GUARD": case "G": if (cont.count() != 1) { String errMsg = String.format( "One guard argument was expected (got %d)", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { String pat = cont.get(0); try { Pattern.compile(pat); } catch (PatternSyntaxException psex) { String errMsg = String.format( "Guard argument '%s' is not a valid regex (%s)", pat, psex.getMessage()); errs.add(new ReplError(lno, pno, errMsg, nam)); } rp.guard = cont.get(0); } break; case "PRIORITY": case "PRIOR": case "P": try { if (cont.count() != 1) { String errMsg = String.format( "One priority argument was expected (got %d", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { rp.priority = Integer.parseInt(cont.get(0)); } } catch (NumberFormatException nfex) { String errMsg = String.format( "'%s' is not a valid priority (must be an integer)", cont.get(0)); errs.add(new ReplError(lno, pno, errMsg, nam)); } break; case "STAGE": case "S": try { if (cont.count() != 1) { String errMsg = String.format( "One stage argument was expected (got %d", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { int tmpStage = Integer.parseInt(cont.get(0)); if (tmpStage < 0) { String errMsg = String.format( "'%s' is not a valid stage (must be a positive integer)", cont.get(0)); errs.add(new ReplError(lno, pno, errMsg, nam)); break; } rp.stage = tmpStage; } } catch (NumberFormatException nfex) { String errMsg = String.format( "'%s' is not a valid stage (must be a positive integer)", cont.get(0)); errs.add(new ReplError(lno, pno, errMsg, nam)); } break; case "MULTITRUE": case "MULTIT": case "MT": isMulti = true; break; case "MULTIFALSE": case "MULTIF": case "MF": isMulti = false; break; case "MULTI": case "M": if (cont.count() != 1) { String errMsg = String.format( "One multi-flag argument was expected (got %d", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { isMulti = Boolean.parseBoolean(cont.get(0)); } break; case "INTERNAL": case "INT": case "I": rp.stat = StageStatus.INTERNAL; break; case "EXTERNAL": case "EXT": case "E": rp.stat = StageStatus.EXTERNAL; break; case "BOTH": case "B": rp.stat = StageStatus.BOTH; break; default: { String errMsg = String.format( "Unknown control name '%s' for name '%s'", cont.name, nam); ReplError erd = new ReplError(lno, pno, errMsg, nam); errs.add(erd); } break; } } name = cs.strang; } // Multi-line name with a trailer if (isMulti) { String tmp = readMultiLine(name, scn, ropts, "name", lno); if (tmp == null) return null; name = tmp; } return name; } private static void readGlobal(String nam, Scanner scn, List errs, ReplOpts ropts, IntHolder lno, IntHolder pno) { ControlledString cs = getControls(nam.substring(1), errs, ropts, lno, pno, "global"); for (Control cont : cs.controls) { switch (cont.name) { case "PRIORITY": case "PRIOR": case "P": try { if (cont.count() != 1) { String errMsg = String.format( "Must specify 1 priority (%d specified)", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { int tmp = Integer.parseInt(cont.get(0)); ropts.defPrior = tmp; } } catch (NumberFormatException nfex) { String errMsg = String.format( "'%s' is not a valid priority (must be an integer)", cont.get(0)); errs.add(new ReplError(lno, pno, errMsg, nam)); } break; case "STAGE": case "S": try { if (cont.count() != 1) { String errMsg = String.format( "Must specify 1 stage (%d specified)", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { int tmpStage = Integer.parseInt(cont.get(0)); if (tmpStage < 0) { String errMsg = String.format( "'%s' is not a valid stage (must be a positive integer)", cont.get(0)); errs.add(new ReplError(lno, pno, errMsg, nam)); break; } ropts.defStage = tmpStage; } } catch (NumberFormatException nfex) { String errMsg = String.format( "'%s' is not a valid stage (must be a positive integer)", cont.get(0)); errs.add(new ReplError(lno, pno, errMsg, nam)); } break; case "MULTITRUE": case "MULTIT": case "MT": ropts.defMulti = true; break; case "MULTIFALSE": case "MULTIF": case "MF": ropts.defMulti = false; break; case "MULTI": case "M": if (cont.count() != 1) { String errMsg = String.format( "Must specify 1 multi-flag (%d specified)", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { ropts.defMulti = Boolean.parseBoolean(cont.get(0)); } break; case "INTERNAL": case "INT": case "I": ropts.defStatus = StageStatus.INTERNAL; break; case "EXTERNAL": case "EXT": case "E": ropts.defStatus = StageStatus.EXTERNAL; break; case "BOTH": case "B": ropts.defStatus = StageStatus.BOTH; break; case "DEBUGTRUE": case "DEBUGT": case "DT": ropts.isDebug = true; break; case "DEBUGFALSE": case "DEBUGF": case "DF": ropts.isDebug = false; break; case "DEBUG": case "D": if (cont.count() != 1) { String errMsg = String.format( "Must specify 1 debug flag (%d specified)", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { ropts.isDebug = Boolean.parseBoolean(cont.get(0)); } break; case "TRACETRUE": case "TRACET": case "TT": ropts.isTrace = true; break; case "TRACEFALSE": case "TRACEF": case "TF": ropts.isTrace = false; break; case "TRACE": case "T": if (cont.count() != 1) { String errMsg = String.format( "Must specify 1 trace flag (%d specified)", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { ropts.isTrace = Boolean.parseBoolean(cont.get(0)); } break; case "PERFTRUE": case "PERFT": case "PRFT": ropts.isPerf = true; break; case "PERFFALSE": case "PERFF": case "PRFF": ropts.isPerf = false; break; case "PERF": case "PRF": if (cont.count() != 1) { String errMsg = String.format( "Must specify 1 perf. flag (%d specified)", cont.count()); errs.add(new ReplError(lno, pno, errMsg, nam)); } else { ropts.isPerf = Boolean.parseBoolean(cont.get(0)); } break; default: { String msg = String.format("Invalid global control name '%s'", cont.name); ReplError err = new ReplError(lno, pno, msg, nam); errs.add(err); } break; } if (ropts.isTrace) ropts.errStream.printf("\t[TRACE] Processed global control '%s'\n", cont); } return; } private static ControlledString getControls(String lne, List errs, ReplOpts ropts, IntHolder lno, IntHolder pno, String type) { try { return ControlledString.parse(lne, new ParseStrings("//", ";", "/", "|")); } catch (IllegalArgumentException iaex) { String msg = "Did not find control terminator (//) in %s where it should be"; msg = String.format(msg, type); ReplError re = new ReplError(lno, pno, msg, lne); errs.add(re); return null; } } }