/* * 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; import com.berryworks.edireader.plugin.LoopDescriptor; import com.berryworks.edireader.plugin.PluginControllerImpl; import com.berryworks.edireader.plugin.PluginPreparation; import com.berryworks.edireader.tokenizer.Tokenizer; import java.util.List; import java.util.Set; /** * Parent class for all EDIReader plugins. * <p> * <b>Naming plugin classes.</b> * The class should extend com.berryworks.edireader.Plugin and must be named * precisely according to a specific pattern. If the plugin is for an * ANSI X.12 transaction set, the class name must be * ANSI_<i>nnn</i> where <i>nnn</i> * is the 3-digit number that appears in the ST segment to designate the particular * transaction set. * For a UN/EDIFACT message type, the class name must be * EDIFACT_<i>name</i> where <i>name</i> * is the symbolic name for the message type that appears in the UNH segment. * For instance, a plugin for an EDIFACT purchase order message would have the * class name EDIFACT_ORDERS. * <p> * EDIReader uses this class name to make the runtime linkage to the plugin. * Once it has parsed the ST segment (or UNH segment for EDIFACT) it * forms the classname that the plugin would have if it is available and * uses the classloader to locate a class of this name in the CLASSPATH. * If no such class exists, then EDIReader continues without a plugin and * generates XML that does not reflect the internal looping structure of the * document. * <p> * For this linkage to work properly, the fully-qualified class name must * match. Therefore, your plugin must bear a package name of * com.berryworks.edireader.plugin * although it can be compiled separately and does not have to be placed * inside any particular jar file. * <p> * <b>LoopDescriptors.</b> * The essence of a Plugin is an array of LoopDescriptors. An EDIReader plugin is expected * to subclass Plugin and initialize the loops array with loop descriptors. * Each LoopDescriptor expresses a specific situation that corresponds to the entry or exit of a * loop instance while parsing. * Refer to the documentation for LoopDescriptor for further details. * <p> * <b>PluginController.</b> * A parser for a particular EDI standard, typically AnsiReader or EdifactReader, * uses an instance of a PluginController to interact with the plugin while parsing the * segments of an EDI document. * For each segment encountered within a document, the parser calls the transition method * on the PluginController to determine if the segment corresponds to a loop transaction, * either the entry of a new loop or the completion of a loop. The transition() method then * in turn calls the query() method on the plugin, which considers the LoopDescriptors * associated with the segment type to determine if one applies to the current context. * The query() method considers the LoopDescriptors in the order in which they were * provided in the loops array. The first one that is determined to "match" the context * expressed in the query arguments is returned by the query method. Therefore, the ordering * of the LoopDescriptors for a given segment type is very important as described in the * documentation for LoopDescriptor. * <p> * <b>Plugin Optimization.</b> * The description above suggests that the entire array of LoopDescriptors in * a plugin is examined serially for each segment within an EDI document. * This is logically but not literally true. * For performance reasons, an optimized form of a plugin is created when a * plugin class is loaded so that the LoopDescriptors associated with a * particular segment type can be accessed efficiently. The PluginPreparation * class is for this purpose. * * @see com.berryworks.edireader.plugin.LoopDescriptor * @see PluginController * @see com.berryworks.edireader.plugin.PluginPreparation */ public abstract class Plugin { public static final String ANY_CONTEXT = "*"; public static final String INITIAL_CONTEXT = "/"; public static final String CURRENT = "."; protected static int pluginsLoaded; /** * LoopDescriptor[] is a table of state transfer information specific to a * particular document type. The transition method uses this table to * determine if a state transition occurred or not. */ protected LoopDescriptor[] loops; protected boolean debug; protected final String documentType; protected final String documentName; protected PluginPreparation optimizedForm; private boolean validating; public Plugin(String documentType, String documentName) { this.documentType = documentType; this.documentName = documentName; pluginsLoaded++; } protected Plugin(String documentType, String documentName, boolean validating) { this(documentType, documentName); this.validating = validating; } /** * Perform any initialization needed for the plugin before use with a new document. * <p> * The only cases where this is needed is for plugins that have state. Most plugins are stateless and therefore * an instance of a plugin can be reused for many documents. However, it is possible to develop a subclass of Plugin * that maintains state. For example, a FilteringPlugin might need to make decisions based on what segment types * have been seen previously in a given document. In such a case, you may override the init() method in order to * reset the state before starting a new document. In developing such a state-bearing plugin, you must carefully * consider thread safety issues for multi-threaded environments. The use of ThreadLocal is recommended in such * cases. */ public void init() { } /** * Get the document type (for example, "837") * * @return The documentType value */ public String getDocumentType() { return documentType; } /** * Get the readable name for the document (for example, "Health Care Claim") * * @return The documentName value */ public String getDocumentName() { return documentName; } /** * Get the array of LoopDescriptors * * @return LoopDescriptors */ public LoopDescriptor[] getLoopDescriptors() { return loops; } /** * Query the plugin about a loop that starts with a designated segment type, * given that you are already within a particular loop. * * @param segment type of segment encountered * @param currentLoopStack stack representing nested loops in current state * @param currentLevel nesting level of current state * @param resultFlags * @return descriptor matching query parameters, or null if none */ public LoopDescriptor query(String segment, String currentLoopStack, int currentLevel, Set<String> resultFlags) { LoopDescriptor result = null; if (debug) trace("plugin query for segment " + segment); if (loops == null) return null; if (optimizedForm == null) throw new RuntimeException("Internal error: plugin not properly constructed"); List<LoopDescriptor> descriptorList = optimizedForm.getList(segment); if (descriptorList == null) { if (debug) trace("No descriptors found"); return null; } if (debug) trace("Number of descriptors found: " + descriptorList.size()); for (LoopDescriptor descriptor : descriptorList) { boolean candidate = matchesWithoutRegardToFlagConditionals(descriptor, segment, currentLoopStack, currentLevel); if (candidate) { // Now check to see if has any flag-related conditions. Set<String> conditions = descriptor.getConditionFlags(); boolean satisfied = true; for (String condition : conditions) { if (resultFlags.contains(condition)) { // The condition is satified. continue; } else { satisfied = false; break; } } if (satisfied) { result = descriptor; break; } } } // A loop descriptor with a null loop name serves as a NOT rule. // No further loop descriptors are considered, and query returns a null indicating that no transition should occur. // This provides a way to express a specific context where the appearance of a segment does NOT mark the entry of a new loop. if (result != null && result.getName() == null) result = null; return result; } private boolean matchesWithoutRegardToFlagConditionals(LoopDescriptor descriptor, String segment, String currentLoopStack, int currentLevel) { if (!descriptor.getFirstSegment().equals(segment)) { throw new RuntimeException("Internal error: optimized plugin structure invalid"); } int levelContext = descriptor.getLevelContext(); if (debug) trace("checking level context " + levelContext); if (levelContext > -1) { if (levelContext == currentLevel) { return true; } return false; } String candidateContext = descriptor.getLoopContext(); if (currentLoopStack == null) currentLoopStack = "*"; if (debug) trace("checking loop context " + candidateContext + " with current loop stack " + currentLoopStack); if (ANY_CONTEXT.equals(candidateContext)) { return true; } else if (candidateContext.startsWith("/") && candidateContext.length() > 1 && currentLoopStack.startsWith(candidateContext)) { if (debug) trace("startsWith satisfied"); return true; } else if (currentLoopStack.endsWith(candidateContext)) { return true; } return false; } private void trace(String s) { EDIReader.trace(s); } public void debug(boolean d) { this.debug = d; } public static int getCount() { return pluginsLoaded; } @Override public String toString() { StringBuilder result = new StringBuilder().append("Plugin ").append(getClass().getName()).append("\n ") .append(getDocumentName()).append(" (").append(getDocumentType()).append(')'); if (loops != null) { for (LoopDescriptor loop : loops) result.append('\n').append(loop.toString()); } return result.toString(); } public void prepare() { optimizedForm = new PluginPreparation(loops); } public boolean isValidating() { return validating; } public PluginControllerImpl createController(String standard, Tokenizer tokenizer) { return new PluginControllerImpl(standard, tokenizer); } protected LoopDescriptor[] concatenate(LoopDescriptor[] descriptorsA, LoopDescriptor[] descriptorsB) { LoopDescriptor[] result = new LoopDescriptor[descriptorsA.length + descriptorsB.length]; int loopsIndex = 0; for (LoopDescriptor d : descriptorsA) { result[loopsIndex++] = d; } for (LoopDescriptor d : descriptorsB) { result[loopsIndex++] = d; } return result; } public String getDocumentVersion() { // Try to derive the version from the name of the class // based on the naming convention used by EDIReader. String result = null; final String simpleName = this.getClass().getSimpleName(); final String[] split = simpleName.split("_"); if (split.length == 4) { result = split[3]; } return result; } public PluginDiff compare(Plugin pluginB) { final PluginDiff result = new PluginDiff(); // not match by default if (pluginB == null) { return result.mismatch("second plugin is null"); } if (!this.getDocumentType().equals(pluginB.getDocumentType())) { return result.mismatch("types differ"); } if (!this.getDocumentName().equals(pluginB.getDocumentName())) { return result.mismatch("names differ"); } final LoopDescriptor[] loopsOfB = pluginB.loops; if (loops == null) { if (loopsOfB != null) { return result.mismatch("second plugin has non-null loops while this plugin does not"); } } else { if (loopsOfB == null) { return result.mismatch("second plugin has null loops while this plugin does not"); } if (loops.length != loopsOfB.length) { return result.mismatch("second plugin has a different number of loops than this plugin"); } // TODO iterator through the loops and check for match } result.setMatch(true); return result; } public static class PluginDiff { private boolean match; private String reason; public boolean isMatch() { return match; } public void setMatch(boolean match) { this.match = match; } public String getReason() { return isMatch() ? "matches" : reason; } public void setReason(String reason) { this.reason = reason; } public PluginDiff mismatch(String s) { setMatch(false); setReason(s); return this; } } }