package com.framsticks.parsers; import com.framsticks.params.*; import com.framsticks.util.Misc; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; import java.util.*; public class MultiParamLoader { public interface StatusListener { void onStatusChange(); } private final static Logger log = LogManager.getLogger(MultiParamLoader.class); /** * The class which name was recently found in the file, to which execution * should be passed. */ public Access getLastAccess() { return lastAccess; } /** * Specifies the condition to break execution. */ public enum Status { None, Finished, BeforeObject, AfterObject, BeforeUnknown, OnComment, OnError, Loading } protected final Map> listeners = new HashMap<>(); /** * Specifies the action that should be taken inside loops. */ private enum LoopAction { Nothing, Break, Continue } protected Access lastAccess; protected static final FramsClass emptyFramsClass = FramsClass.build().idAndName("").finish(); /** * Empty Param representing unknown classes - used to omit unknown * objects in the file. */ protected Access emptyParam = new PropertiesAccess(emptyFramsClass); /** * Last comment found in the file. */ protected String lastComment; /** * Set of break conditions. */ private EnumSet breakConditions = EnumSet.of(Status.None); /** * Status of current execution. */ private Status status = Status.None; /** * File from which data should be read. */ private Source currentSource; protected String currentLine; /** * All the files read by the loader (there could be many of them because of * '#|include'). */ private Stack fileStack = new Stack(); /** * A map that specifies connection between the getName of the file and the * actual reader. */ private Map fileMap = new HashMap(); /** * List of known classes. */ protected AccessProvider accessProvider = null; /** * Last unknown object found in the file. */ private String lastUnknownObjectName; /** * @return the currentLine */ public String getCurrentLine() { return currentLine; } /** * @return the accessProvider */ public AccessProvider getAccessProvider() { return accessProvider; } /** * @param accessProvider the accessProvider to set */ public void setAccessProvider(AccessProvider accessProvider) { this.accessProvider = accessProvider; } public MultiParamLoader() { } /** * Starts reading the file. */ public Status go() { Misc.throwIfNull(accessProvider); log.trace("go"); while (!isFinished()) { // check if we are before some known or unknown object LoopAction loopAction = tryReadObject(); if (loopAction == LoopAction.Break) { break; } else if (loopAction == LoopAction.Continue) { continue; } // read data currentLine = currentSource.readLine(); // end of file if (currentLine == null) { if (!returnFromIncluded()) { finish(); break; } else { continue; } } log.trace("read line: {}", currentLine); // empty line if (currentLine.length() == 0) { continue; } // check if some file should be included if (isIncludeLine(currentLine) == LoopAction.Continue) { continue; } // check if should break on comment if (isCommentLine(currentLine) == LoopAction.Break) { break; } // get class getName LoopAction action = changeCurrentParamInterface(currentLine); if (action == LoopAction.Break) { break; } if (action == LoopAction.Continue) { continue; } // log.warn("unknown line: {}", currentLine); changeStatus(Status.OnError); if (action == LoopAction.Break) { break; } if (action == LoopAction.Continue) { continue; } } return status; } /** * Checks whether the reader found a known or unknown object and execution * should be passed to it. * @throws Exception */ private LoopAction tryReadObject() { if (status == Status.BeforeObject || (status == Status.BeforeUnknown && lastAccess != null)) { // found object - let it load data if (lastAccess.getSelected() == null) { lastAccess.select(lastAccess.createAccessee()); } log.trace("loading into {}", lastAccess); AccessOperations.load(lastAccess, currentSource); if (changeStatus(Status.AfterObject)) { return LoopAction.Break; } return LoopAction.Continue; } else if (status == Status.BeforeUnknown) { log.warn("omitting unknown object: {}", lastUnknownObjectName); // found unknown object AccessOperations.load(emptyParam, currentSource); if (changeStatus(Status.AfterObject)) { return LoopAction.Break; } return LoopAction.Continue; } return LoopAction.Nothing; } /** * Checks whether some additional file shouldn't be included. */ private LoopAction isIncludeLine(String line) { try { // found comment if (line.charAt(0) == '#') { // maybe we should include something if (line.substring(1, 8).equals("include")) { int beg = line.indexOf('\"'); if (beg == -1) { log.info("Wanted to include some file, but the format is incorrect"); return LoopAction.Continue; } String includeFileName = line.substring(beg + 1); int end = includeFileName.indexOf('\"'); if (end == -1) { log.info("Wanted to include some file, but the format is incorrect"); return LoopAction.Continue; } includeFileName = includeFileName.substring(0, end); include(includeFileName); return LoopAction.Continue; } } } catch (IndexOutOfBoundsException ex) { // value after # sign is shorter than expected 7 characters - do // nothing } return LoopAction.Nothing; } /** * Checks whether execution shouldn't break on comment. */ private LoopAction isCommentLine(String line) { if (line.charAt(0) == '#') { lastComment = line; if (changeStatus(Status.OnComment)) { // it's a simple comment - maybe we should break? return LoopAction.Break; } } return LoopAction.Nothing; } /** * Gets the getName of the class from line read from file. */ private LoopAction changeCurrentParamInterface(String line) { // found key - value line if (line.charAt(line.length() - 1) == ':') { String typeName = line.substring(0, line.length() - 1); lastAccess = accessProvider.getAccess(typeName); if (lastAccess != null) { if (changeStatus(Status.BeforeObject)) { return LoopAction.Break; } else { return LoopAction.Continue; } } else { lastUnknownObjectName = typeName; if (changeStatus(Status.BeforeUnknown)) { return LoopAction.Break; } else { return LoopAction.Continue; } } } return LoopAction.Nothing; } /** * Adds another break condition. */ public void addBreakCondition(Status condition) { breakConditions.add(condition); } /** * Removes break condition. */ public void removeBreakCondition(Status condition) { breakConditions.remove(condition); } /** * Checks whether execution is finished. */ private boolean isFinished() { return (status == Status.Finished); } private void finish() { log.trace("finishing"); if (currentSource != null) { currentSource.close(); } changeStatus(Status.Finished); } /** * Opens selected file. */ public boolean setNewSource(Source source) { log.debug("switching current source to {}...", source.getFilename()); currentSource = source; changeStatus(Status.Loading); return true; } /** * Includes specified file. */ private void include(String includeFilename) { includeFilename = currentSource.demangleInclude(includeFilename); if (includeFilename == null) { return; } // check if it is already included and break if it is if (isAlreadyIncluded(includeFilename)) { log.debug("circular reference ignored ({})", includeFilename); return; } log.info("including file {}...", includeFilename); Source newSource = currentSource.openInclude(includeFilename); if (newSource == null) { return; } fileStack.add(currentSource.getFilename()); fileMap.put(currentSource.getFilename(), currentSource); setNewSource(newSource); } /** * Checks whether selected file was already included. */ private boolean isAlreadyIncluded(String filename) { for (String file : fileStack) { if (filename.equals(file)) { log.warn("file {} was already included", filename); return true; } } return false; } /** * Returns from included file. */ private boolean returnFromIncluded() { if (fileStack.size() == 0) { return false; } if (currentSource != null) { currentSource.close(); } String filename = fileStack.pop(); currentSource = fileMap.get(filename); fileMap.remove(filename); return true; } /** * Checks whether execution should break on selected condition. */ private boolean changeStatus(Status status) { log.trace("changing status: {} -> {}", this.status.toString(), status.toString()); this.status = status; if (listeners.containsKey(status)) { for (StatusListener l : listeners.get(status)) { l.onStatusChange(); } } return breakConditions.contains(status); } public Object returnObject() { assert lastAccess != null; Object result = lastAccess.getSelected(); if (result == null) { return null; } lastAccess.select(null); return result; } public void addListener(Status status, StatusListener listener) { if (!listeners.containsKey(status)) { listeners.put(status, new LinkedList()); } listeners.get(status).add(listener); } public static List loadAll(Source source, Access access) { final List result = new LinkedList<>(); final MultiParamLoader loader = new MultiParamLoader(); loader.setNewSource(source); loader.setAccessProvider(new AccessStash().add(access)); loader.addListener(MultiParamLoader.Status.AfterObject, new StatusListener() { @Override public void onStatusChange() { result.add(loader.returnObject()); } }); loader.go(); return result; } }