/* * Copyright 2005-2016 by BerryWorks Software, LLC. All rights reserved. * * This file is part of EDIReader. You may obtain a license for its use directly from * BerryWorks Software, and you may also choose to use this software under the terms of the * GPL version 3. Other products in the EDIReader software suite are available only by licensing * with BerryWorks. Only those files bearing the GPL statement below are available under the GPL. * * EDIReader is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * EDIReader 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along with EDIReader. If not, * see <http://www.gnu.org/licenses/>. */ package com.berryworks.edireader.plugin; import com.berryworks.edireader.EDISyntaxException; import com.berryworks.edireader.Plugin; import com.berryworks.edireader.PluginController; import com.berryworks.edireader.tokenizer.Tokenizer; import java.util.HashSet; import java.util.Set; import static com.berryworks.edireader.Plugin.CURRENT; /** * Determines and maintains state transitions for the segment looping structure * within a particular EDI document. * <p> * An EDI parser delegates the job of detecting segment loop boundaries * to a PluginController. This allows the EDI parsers for ANSI and EDIFACT * to be fully consistent with their use of plugins and focus on the specifics of * the particular EDI standard. * <p> * This PluginControllerImpl provides the normal * segment loop support based on LoopDescriptors in Plugins. * It is possible, however, to extend this behavior by creating a subclass of * PluginControllerImpl and Plugin. A ValidatingPlugin is one example, * which provides for certain EDI validation rules, beyond those applied by * normal EDIReader parsing, to be applied while a document is being parsed. * Another example is a FilteringPlugin, which allows a plugin to provide * custom logic to filter out certain LoopDescriptors based on run-time decisions. * * @see com.berryworks.edireader.Plugin * @see com.berryworks.edireader.plugin.LoopDescriptor */ public class PluginControllerImpl extends PluginController { protected boolean enabled; protected final String standard; protected String documentType; protected Plugin plugin; protected final LoopStack loopStack = new LoopStack(); protected final Tokenizer tokenizer; /** * Name of the current loop. The implicit outer loop is represented by the * name "/". */ protected String currentLoopName = "/"; /** * Descriptor that caused us to enter the loop we are now in. */ protected LoopDescriptor loopDescriptor = new LoopDescriptor( currentLoopName, "", 0, "/"); /** * Number of loops that were closed as the result of the most recent * transition. A transition that re-enters the implicit outer loop does not * consider the outer loop in this count. */ protected int numberOfLoopsClosed; private final Set<String> resultFlags = new HashSet<>(); /** * Construct a PluginControllerImpl * * @param standard - name of EDI standard (for example: "EDIFACT" or "ANSI") * @param tokenizer - reference to the Tokenizer to provide context for syntax exceptions */ public PluginControllerImpl(String standard, Tokenizer tokenizer) { this.standard = standard; this.tokenizer = tokenizer; } /** * Compute a state transition that may have occurred as the result of the * presence of a particular segment type at this point in parsing the * document. * * @param segmentName type of segment encountered, for example: 837 * @return true if there was a transition to a new loop, false otherwise * @throws com.berryworks.edireader.EDISyntaxException Description of the Exception */ @Override public boolean transition(String segmentName) throws EDISyntaxException { if (!enabled) return false; if (debug) trace("considering segment " + segmentName + " while in loop " + loopDescriptor.getName() + " with stack " + loopStack.toString()); boolean result = false; LoopDescriptor newDescriptor = plugin.query( segmentName, loopStack.toString(), loopDescriptor.getNestingLevel(), resultFlags); if (debug) trace("considering segment " + segmentName + " using descriptor " + newDescriptor); if (!validateDescriptor(newDescriptor, segmentName, tokenizer)) return false; // Set flags related to this descriptor. Set<String> flags = newDescriptor.getResultFlags(); for (String flagName : flags) { if (debug) trace("setting flag " + flagName); resultFlags.add(flagName); } String newLoopName = newDescriptor.getName(); if (CURRENT.equals(newLoopName) && newDescriptor.getNestingLevel() == loopDescriptor.getNestingLevel()) { if (debug) trace("resuming current loop without transition"); } else { if (debug) trace("transitioning to level " + newDescriptor.getNestingLevel()); result = true; numberOfLoopsClosed = loopDescriptor.getNestingLevel() - newDescriptor.getNestingLevel(); boolean resumeLoop = true; if (newLoopName.startsWith("/")) // Resume the current loop at the target level with // closing it. currentLoopName = newLoopName; else if (newLoopName.startsWith(".")) { // Resume the current loop at the target level with // closing it. currentLoopName = newDescriptor.getNestingLevel() == 0 ? "/" : "."; } else { // Close the current loop at the target level so that we can initiate a new one. numberOfLoopsClosed++; currentLoopName = newLoopName; resumeLoop = false; } if (debug) trace("closing " + numberOfLoopsClosed + " loops"); if ((numberOfLoopsClosed < 0) || (numberOfLoopsClosed > loopDescriptor.getNestingLevel())) throw new EDISyntaxException("Improper sequencing noted with segment " + segmentName, tokenizer); else if (numberOfLoopsClosed > 0) { // Pop that many off the tack for (int i = 0; i < numberOfLoopsClosed; i++) { LoopContext completedLoop = loopStack.pop(); validateCompletedLoop(completedLoop); if (debug) trace("popped " + completedLoop + " off the stack"); } } loopDescriptor = newDescriptor; if (resumeLoop) { if (debug) trace("resuming loop at level " + loopDescriptor.getNestingLevel() + " with name " + loopDescriptor.getName()); if (loopDescriptor.getNestingLevel() == 0 && loopDescriptor.getName().length() > 1 && loopDescriptor.getName().startsWith("/")) { if (debug) trace("special legacy case: " + loopDescriptor); loopStack.setBottom(new LoopContext(loopDescriptor.getName().substring(1))); } } else { loopStack.push(createLoopContext(loopDescriptor.getName(), plugin, loopStack.toString())); if (debug) trace("pushed " + loopDescriptor.getName() + " onto the stack"); } } return validateSegment(newDescriptor, loopStack, tokenizer) && result; } /** * Used only within the internal implementation of this class and its subclasses. * * @param name - loop name * @param plugin - this controller's Plugin * @param stack - LoopStack expressed with toString() * @return LoopContext */ protected LoopContext createLoopContext(String name, Plugin plugin, String stack) { return new LoopContext(name); } /** * Performs validation logic with respect to a candidate LoopDescriptor. * <p> * The default implementation provide by this class returns true, indicating that the * LoopDescriptor is valid, for any non-null LoopDescriptor. * The primary purpose of this method is to provide a "hook" so that a subclass of * PluginController can override this method and apply custom logic. * * @param descriptor - LoopDescriptor governing the appearance of this segment * @param segmentName - name of the segment * @param tokenizer - reference to the Tokenizer to provide context for syntax exceptions * @return true if the LoopDescriptor is considered valid. * @throws EDISyntaxException - corresponding to a validation fault */ protected boolean validateDescriptor(LoopDescriptor descriptor, String segmentName, Tokenizer tokenizer) throws EDISyntaxException { return descriptor != null; } /** * Performs validation logic appropriate for the end of a segment loop. * <p> * The default implementation is empty. A PluginController subclass can override * this method in order to apply its own validation policies. * * @param completedLoop - describes the loop that was just completed * @throws EDISyntaxException - corresponding to a validation fault */ protected void validateCompletedLoop(LoopContext completedLoop) throws EDISyntaxException { } /** * Validates the appearance of a segment, given the selected LoopDescriptor and the current LoopStack. * <p> * The default implementation always returns true. * The primary purpose of this method is to provide a hook for subclasses to perform * a particular type of validation. * * @param descriptor - the LoopDescriptor for the loop in which this segment appears * @param loopStack - LoopStack corresponding to the current parsing state * @param tokenizer - reference to the Tokenizer to provide context for syntax exceptions * @return true if the segment appearance is valid * @throws EDISyntaxException - thrown if an invalid condition is detected */ protected boolean validateSegment(LoopDescriptor descriptor, LoopStack loopStack, Tokenizer tokenizer) throws EDISyntaxException { return true; } /** * Returns the LoopStack. * * @return LoopStack */ protected LoopStack getLoopStack() { return loopStack; } /** * Return the name of a loop that was entered as the result of the most * recent transition. * * @return name of the entered loop, or null if no loop was entered */ @Override public String getLoopEntered() { return currentLoopName; } /** * Get the number of loops that were closed as the result of the most recent * state transition. Re-entering the implicit outer loop does not count as a * loop closing. * * @return Description of the Return Value */ @Override public int closedCount() { return numberOfLoopsClosed; } /** * Get the nesting level of the current loop. * * @return Description of the Return Value */ @Override public int getNestingLevel() { return loopDescriptor.getNestingLevel(); } /** * Returns true if this controller is currently enabled. * * @return true if currently enabled */ @Override public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * Returns the document name associated with the plugin. * * @return name of the document */ @Override public String getDocumentName() { return enabled ? plugin.getDocumentName() : null; } /** * Returns the Plugin used by this PluginController. * * @return Plugin */ @Override public Plugin getPlugin() { return plugin; } public void setPlugin(Plugin plugin) { this.plugin = plugin; } /** * Returns true if the most recent loop transition was to resume an outer loop. * * @return true if the transition was to an outer loop */ @Override public boolean isResumed() { String s = getLoopEntered(); return s.startsWith("/") || ".".equals(s); } public String getDocumentType() { return documentType; } public void setDocumentType(String documentType) { this.documentType = documentType; } }