ReplPair.java
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<ReplPair>, UnaryOperator<String> {
// 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<ReplPair> readList(Scanner scn) {
List<ReplPair> 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<ReplPair> readList(List<ReplPair> detals, Scanner scn) {
List<ReplError> errList = new ArrayList<>();
List<ReplPair> 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<ReplPair> readList(List<ReplPair> detals, Scanner scn,
List<ReplError> 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<ReplPair> readList(List<ReplPair> detals, Scanner scn,
List<ReplError> errs, ReplOpts ropts) {
IntHolder lno = new IntHolder();
IntHolder pno = new IntHolder();
List<List<ReplPair>> stages = new ArrayList<>();
stages.add(new ArrayList<ReplPair>());
// 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<ReplPair> 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<ReplPair> tmpList = new ArrayList<>();
tmpList.addAll(detals);
if (ropts.isTrace)
ropts.errStream.printf("\t[DEBUG] Stages: %s\n", stages);
int procStg = 0;
for (List<ReplPair> stageList : stages) {
procStg += 1;
List<ReplPair> 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<ReplError> 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<ReplError> 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<ReplError> 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;
}
}
}