package qz.build.provision; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import qz.build.provision.params.Arch; import qz.build.provision.params.Os; import qz.build.provision.params.Phase; import qz.build.provision.params.Type; import qz.build.provision.params.types.Remover; import qz.build.provision.params.types.Software; import java.nio.file.Path; import java.util.HashSet; import java.util.LinkedList; import java.util.List; public class Step { protected static final Logger log = LogManager.getLogger(Step.class); String description; Type type; List args; // Type.SCRIPT or Type.INSTALLER or Type.CONF only HashSet os; HashSet arch; Phase phase; String data; Path relativePath; Class relativeClass; public Step(Path relativePath, String description, Type type, HashSet os, HashSet arch, Phase phase, String data, List args) { this.relativePath = relativePath; this.description = description; this.type = type; this.os = os; this.arch = arch; this.phase = phase; this.data = data; this.args = args; } /** * Only should be used by unit tests */ Step(Class relativeClass, String description, Type type, HashSet os, HashSet arch, Phase phase, String data, List args) { this.relativeClass = relativeClass; this.description = description; this.type = type; this.os = os; this.arch = arch; this.phase = phase; this.data = data; this.args = args; } @Override public String toString() { return "Step { " + "description=\"" + description + "\", " + "type=\"" + type + "\", " + "os=\"" + Os.serialize(os) + "\", " + "arch=\"" + Arch.serialize(arch) + "\", " + "phase=\"" + phase + "\", " + "data=\"" + data + "\", " + "args=\"" + StringUtils.join(args, ",") + "\" " + "}"; } public JSONObject toJSON() throws JSONException { JSONObject json = new JSONObject(); json.put("description", description) .put("type", type) .put("os", Os.serialize(os)) .put("arch", Arch.serialize(arch)) .put("phase", phase) .put("data", data); for(int i = 0; i < args.size(); i++) { json.put(String.format("arg%s", i + 1), args.get(i)); } return json; } public String getDescription() { return description; } public Type getType() { return type; } public void setType(Type type) { this.type = type; } public List getArgs() { return args; } public void setArgs(List args) { this.args = args; } public HashSet getOs() { return os; } public void setOs(HashSet os) { this.os = os; } public HashSet getArch() { return arch; } public void setArch(HashSet arch) { this.arch = arch; } public Phase getPhase() { return phase; } public void setPhase(Phase phase) { this.phase = phase; } public String getData() { return data; } public void setData(String data) { this.data = data; } public Class getRelativeClass() { return relativeClass; } public Path getRelativePath() { return relativePath; } public boolean usingClass() { return relativeClass != null; } public boolean usingPath() { return relativePath != null; } public static Step parse(JSONObject jsonStep, Object relativeObject) { String description = jsonStep.optString("description", ""); Type type = Type.parse(jsonStep.optString("type", null)); String data = jsonStep.optString("data", null); // Handle installer args List args = new LinkedList<>(); if(type == Type.SOFTWARE || type == Type.CONF) { // Handle space-delimited args args = Software.parseArgs(jsonStep.optString("args", "")); // Handle standalone single args (won't break on whitespace) // e.g. "arg1": "C:\Program Files\Foo" int argCounter = 0; while(true) { String singleArg = jsonStep.optString(String.format("arg%d", ++argCounter), ""); if(!singleArg.trim().isEmpty()) { args.add(singleArg.trim()); } else { // stop searching if the next incremental arg (e.g. "arg2") isn't found break; } } } // Mandate "args" as the CONF path if(type == Type.CONF) { // Honor "path" first, if provided String path = jsonStep.optString("path", ""); if(!path.isEmpty()) { args.add(0, path); } // Keep only the first value if(args.size() > 0) { args = args.subList(0, 1); } else { throw formatted("Conf path value cannot be blank."); } } HashSet os = new HashSet<>(); if(jsonStep.has("os")) { // Do not tolerate bad os values String osString = jsonStep.optString("os"); os = Os.parse(osString); if(os.size() == 0) { throw formatted("Os provided '%s' could not be parsed", osString); } } HashSet arch = new HashSet<>(); if(jsonStep.has("arch")) { // Do not tolerate bad arch values String archString = jsonStep.optString("arch"); arch = Arch.parse(archString); if(arch.size() == 0) { throw formatted("Arch provided \"%s\" could not be parsed", archString); } } Phase phase = null; if(jsonStep.has("phase")) { String phaseString = jsonStep.optString("phase", null); phase = Phase.parse(phaseString); if(phase == null) { log.warn("Phase provided \"{}\" could not be parsed", phaseString); } } Step step; if(relativeObject instanceof Path) { step = new Step((Path)relativeObject, description, type, os, arch, phase, data, args); } else if(relativeObject instanceof Class) { step = new Step((Class)relativeObject, description, type, os, arch, phase, data, args); } else { throw formatted("Parameter relativeObject must be of type 'Path' or 'Class' but '%s' was provided", relativeObject.getClass()); } return step.sanitize(); } private Step sanitize() { return throwIfNull("Type", type) .throwIfNull("Data", data) .validateOs() .validateArch() .enforcePhase(Type.PREFERENCE, Phase.STARTUP) .enforcePhase(Type.CA, Phase.CERTGEN) .enforcePhase(Type.CERT, Phase.STARTUP) .enforcePhase(Type.CONF, Phase.CERTGEN) .enforcePhase(Type.SOFTWARE, Phase.INSTALL) .enforcePhase(Type.REMOVER, Phase.INSTALL) .enforcePhase(Type.PROPERTY, Phase.CERTGEN, Phase.INSTALL) .validateRemover(); } private Step validateRemover() { if(type != Type.REMOVER) { return this; } Remover remover = Remover.parse(data); switch(remover) { case CUSTOM: break; default: if(remover.matchesCurrentSystem()) { throw formatted("Remover '%s' would conflict with this installer, skipping. ", remover); } return this; } // Custom removers must have three elements if(data == null || data.split(",").length != 3) { throw formatted("Remover data '%s' is invalid. Data must match a known type [%s] or contain exactly 3 elements.", data, Remover.valuesDelimited(",")); } return this; } private Step throwIfNull(String name, Object value) { if(value == null) { throw formatted("%s cannot be null", name); } return this; } private Step validateOs() { if(os == null) { if(type == Type.SOFTWARE) { // Software must default to a sane operating system os = Software.parse(data).defaultOs(); } else { os = new HashSet<>(); } } if(os.size() == 0) { os.add(Os.ALL); log.debug("Os list is null, assuming '{}'", Os.ALL); } return this; } private Step validateArch() { if(arch == null) { arch = new HashSet<>(); } if(arch.size() == 0) { arch.add(Arch.ALL); log.debug("Arch list is null, assuming '{}'", Arch.ALL); } return this; } private Step enforcePhase(Type matchType, Phase ... requiredPhases) { if(requiredPhases.length == 0) { throw new UnsupportedOperationException("At least one Phase must be specified"); } if(type == matchType) { for(Phase requiredPhase : requiredPhases) { if (phase == null) { phase = requiredPhase; log.debug("Phase is null, defaulting to '{}' based on Type '{}'", phase, type); return this; } else if (phase == requiredPhase) { return this; } } log.debug("Phase '{}' is unsupported for Type '{}', defaulting to '{}'", phase, type, phase = requiredPhases[0]); } return this; } private static UnsupportedOperationException formatted(String message, Object ... args) { String formatted = String.format(message, args); return new UnsupportedOperationException(formatted); } Step cloneTo(Phase phase) { return relativePath != null ? new Step(relativePath, description, type, os, arch, phase, data, args) : new Step(relativeClass, description, type, os, arch, phase, data, args); } public Step clone() { return cloneTo(this.phase); } }