/*
* Copyright 2005-2015 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.Plugin;
import java.util.Set;
import java.util.TreeSet;
import static com.berryworks.edireader.util.FixedLength.isPresent;
/**
* Static metadata about a segment loop (also known as a segment group) within an EDI document
* (also known as transaction set or message). A sequence of LoopDescriptors
* comprise the essence of a transaction set Plugin which allows a subclass of
* EDIReader to consider the nested segment loops that are often important to
* the semantics of a document.
* <p>
* <b>Overview.</b>
* An instance of a LoopDescriptor in a plugin is an expression of a rule.
* Generally speaking, when an EDIReader parser is parsing an EDI document
* for which a plugin is available, it consults these LoopDescriptor rules
* for each segment in the document to determine if the appearance of the
* segment marks the beginning of a new segment loop.
* <p>
* For each segment, the parser evaluates each rule in order
* and performs the action described
* by the first rule that applies in the current context.
* Any remaining rules are ignored, and if no rule is found that applies
* to the segment in current context, then no action is taken;
* in other words, the segment is treated simply as another segment in the
* current segment loop.
* <p>
* <b>Basic attributes of a LoopDescriptor.</b>
* There are four attributes of a LoopDescriptor which are specified in its constructor.
* <p>
* The <i>name</i> is simply the name of a segment loop described by this rule.
* This name will appear as the value of the "Id" attribute in the loop element of the
* generated XML.
* There are a few special cases for this attribute that are described below.
* <p>
* The <i>nestingLevel</i> indicates how deeply nested is the loop. The segments of an
* EDI document that are not within a segment loop are considered to be
* in an implicit outer loop at nesting level 0.
* <p>
* The <i>firstSegment</i> attribute is the type of the segment that
* marks the beginning of a new instance of the described segment loop.
* For example a "PID" value might be used in a plugin for an ANSI 810 Invoice
* document to define the segment loop that begins with the PID segment.
* Similarly, a "LIN" value might be used in a plugin for
* an EDIFACT INVOIC message to define a loop that begins with an LIN segment.
* <p>
* The <i>loopContext</i> attribute places a condition on when the
* LoopDescriptor applies as described below.
* <p><b>When does a LoopDescriptor rule apply?</b>
* When the parser encounters a new segment while parsing an
* EDI document, it consults LoopDescriptor rules to discover one that might apply.
* When determining if a particular rule applies, only the firstSegment
* and loopContext attributes are considered;
* the other two attributes govern the action take once a rule is selected
* and have no bearing on the decision of whether or not the rule applies.
* <p>
* For a rule to apply, the firstSegment attribute must match exactly the
* segment type of the segment in the document.
* Once the segment type is matched to the firstSegment attribute,
* the loopContext is considered.
* The value "*",
* which can be specified by ANY_CONTEXT symbolic constant,
* indicates that this LoopDescriptor rule applies any time that the
* segment in the EDI document matches the firstSegment attribute.
* <p>
* The loopContext attribute may also begin with a "/" and specify
* a nested loop path of slash-delimited loop names. For example, a loopContext
* of "/ABC/DEF/GHI" would indicate a GHI loop nested within a DEF loop
* nested within an ABC loop which is nested within the implicit outer loop
* of the document.
* A segment appearing in an EDI document is considered to match such a
* LoopDescriptor rule if the segment type matches the firstSegment attribute
* and the nested segment loop context of that segment in the parsed document
* is included in the context described by the path.
* <p>
* <b>Actions described by a LoopDescriptor.</b>
* The most obvious action is for the parser to start a new instance of a segment loop,
* and this action is designated by simply using the desired loop name as the loopName
* attribute.
* It is common in some EDI scenarios for loops to be named after the first segment in the loop
* but this is not necessary.
* If the new loop instance is at a lower nesting level than the current loop, then the parser
* generates the proper XML to terminate the current loops as needed.
* In this way, the LoopDescriptor rules need only describe the conditions for entering a
* new segment loop, and the parser and supporting framework can determine where the end of
* each segment loop occurs.
* <p>
* A loopName of null indicates an explicit non-action. When a rule of this kind is applied,
* then no loop transition occurs in the EDI document, and since a matching rule was encountered
* no further are considered. By placing a null action rule in the sequence of rules for a given
* segment type, you can in effect eliminate certain conditions and therefore simplify the
* rules that follow.
* <p>
* The value "/" as a loopName indicates re-entry of the current loop at the designated nesting level.
* The appropriate number of nested loops are properly terminated.
* <p>
* <b>Ordering of LoopDescriptors in a Plugin.</b>
* Remember that the parser considers LoopDescriptors in the order that they appear
* in the plugin and accepts the first one that matches, ignoring the rest.
* Therefore, if there are multiple LoopDescriptors for a given segment type,
* the relative order in which those LoopDescriptors appear is very important
* to the semantics of the plugin. Whether all of the LoopDescriptors for a given
* segment type are grouped together or scattered throughout the LoopDescriptor array is
* not important, only the relative order of LoopDescriptors with respect to others
* for the same segment type. (For readability, it is suggested that you group LoopDescriptors
* for a given segment type together, and order those groups alphabetically by segment type.)
* <p>
* Within a series of LoopDescriptors for a particular segment type,
* it is usually a good idea to place those descriptors with the deepest nesting level
* and longest context path first. In this way, the parser will consider the most
* specific rules first and, if none of these apply, fall through to the more general rules
* that cover the "else" conditions.
* This can simplify the context argument for LoopDescriptors. In fact, it is common for the last
* LoopDescriptor for a given segment type to use ANY_CONTEXT for its context argument, since the
* prior LoopDescriptors would have covered all possibilities but one.
* <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
* @see com.berryworks.edireader.plugin.PluginPreparation
*/
public class LoopDescriptor {
/**
* Name of the loop entered as the result of applying this descriptor.
* <p>
* The name is typically a simple text name that appears in the
* specifications of the EDI document but not in the actual data. The value
* "/" in this field designates the implicit outer loop. The variant form
* /segmentname designates the outer loop following the appearance of the
* named segment.
*/
protected String name;
/**
* Segment type of the first segment in this loop.
*/
protected final String firstSegment;
/**
* Level of nesting for this loop.
* <p>
* Typically, a document will begin with one or more segments that are
* considered to be outside of any segment loops. The segments are
* implicitly at nesting level 0. The first level of explicit nesting is
* level 1, and increases with each new level.
*/
protected final int nestingLevel;
/**
* Context which determines whether or not a particular appearance of a
* segment type that matches the firstSegment is in fact an instance of this
* loop.
*/
protected String loopContext;
private final int levelContext;
/**
* One or flags which may be set as the result of encountering the loop.
* These flags may be used as conditions within the context for the loop.
* resultFlags are the flags that are set when the rule (loop descriptor) is fired
* conditionFlags are the flags that must be set for the descriptor's condition to be satisfied
*/
private final Set<String> resultFlags = new TreeSet<>();
private final Set<String> conditionFlags = new TreeSet<>();
/**
* Constructor a descriptor for recognizing the beginning of a nested loop.
*
* @param loopName Name of the loop, suitable for use as an XML attribute value
* @param firstSegment Segment type that (at least sometimes) indicates entry into
* this loop.
* @param nestingLevel How deeply is this loop nested within other loops.
* @param currentLoop Name of a loop; indicates a valid prior state
*/
public LoopDescriptor(String loopName, String firstSegment, int nestingLevel, String currentLoop) {
this.name = loopName;
this.firstSegment = firstSegment;
this.nestingLevel = nestingLevel;
this.loopContext = currentLoop;
this.levelContext = -1;
lookForFlags();
}
/**
* Equivalent to LoopDescriptor(loopName, firstSegment, nestingLevel, ANY_CONTEXT)
*
* @param loopName Name of the loop, suitable for use as an XML attribute value
* @param firstSegment Segment type that (at least sometimes) indicates entry into
* this loop.
* @param nestingLevel How deeply is this loop nested within other loops.
*/
public LoopDescriptor(String loopName, String firstSegment, int nestingLevel) {
this(loopName, firstSegment, nestingLevel, Plugin.ANY_CONTEXT);
}
public LoopDescriptor(String loopName, String firstSegment) {
this(loopName, firstSegment, 1, Plugin.INITIAL_CONTEXT);
}
public LoopDescriptor(String loopName, String firstSegment, int nestingLevel, int currentLevel) {
this.name = loopName;
this.firstSegment = firstSegment;
this.nestingLevel = nestingLevel;
this.loopContext = Plugin.ANY_CONTEXT;
this.levelContext = currentLevel;
lookForFlags();
}
private void lookForFlags() {
// Look for name+flag+flag pattern in name
if (isPresent(name)) {
int indexOfFirstPlus = name.indexOf('+');
if (indexOfFirstPlus > 0) {
boolean first = true;
for (String part : name.split("\\+")) {
if (first) {
name = part;
first = false;
} else {
resultFlags.add(part);
}
}
}
}
// Look for text?flag?flag pattern in the loop context
if (isPresent(loopContext)) {
int indexOfFirstQuestion = loopContext.indexOf('?');
if (indexOfFirstQuestion > 0) {
boolean first = true;
for (String part : loopContext.split("\\?")) {
if (first) {
loopContext = part;
first = false;
} else {
conditionFlags.add(part);
}
}
}
}
}
/**
* Get the name of the loop.
*
* @return String
*/
public String getName() {
return name;
}
/**
* Get the nested loop depth for this loop within the document.
*
* @return int nestingLevel value
*/
public int getNestingLevel() {
return nestingLevel;
}
public String getLoopContext() {
return loopContext;
}
public int getLevelContext() {
return levelContext;
}
/**
* Get the segment type of the first segment in the loop which, in context,
* defines an occurrence of the loop.
*
* @return The firstSegment value
*/
public String getFirstSegment() {
return firstSegment;
}
/**
* Returns a String representation of this LoopDescriptor
* for testing and debugging purposes.
*
* @return String representation
*/
@Override
public String toString() {
String result = "loop " + getName() + " at nesting level " + getNestingLevel()
+ ": encountering segment " + getFirstSegment();
String context = getLoopContext();
switch (context) {
case "*":
result += " anytime";
break;
case "/":
result += " while outside any loop";
break;
default:
result += " while currently in loop " + context;
break;
}
if (levelContext > -1)
result += " while current at nesting level " + levelContext;
if (!resultFlags.isEmpty()) {
result += ", setting";
for (String flagName : resultFlags) {
result += " " + flagName;
}
}
if (!conditionFlags.isEmpty()) {
result += ", conditional based on";
for (String flagName : conditionFlags) {
result += " " + flagName;
}
}
return result;
}
/**
* Overrides equals
*
* @param target - the reference object with which to compare.
* @return true if this object is the same as the obj argument; false otherwise.
*/
public boolean equals(Object target) {
if (!(target instanceof LoopDescriptor))
return false;
LoopDescriptor sld = (LoopDescriptor) target;
return equalsOrBothNull(getName(), sld.getName())
&& equalsOrBothNull(getFirstSegment(), sld.getFirstSegment())
&& getNestingLevel() == sld.getNestingLevel()
&& getLoopContext().equals(sld.getLoopContext())
&& getLevelContext() == sld.getLevelContext()
&& getResultFlags().equals(sld.getResultFlags())
&& getConditionFlags().equals(sld.getConditionFlags());
}
public int hashCode() {
return getName().hashCode() + getFirstSegment().hashCode();
}
private boolean equalsOrBothNull(String value1, String value2) {
return value1 == null && value2 == null || value1 != null && value1.equals(value2);
}
public boolean isAnyContext() {
return Plugin.ANY_CONTEXT.equals(loopContext);
}
public boolean isResultFlag(String flagName) {
return resultFlags.contains(flagName);
}
public Set<String> getResultFlags() {
return resultFlags;
}
public boolean isConditionFlag(String flagName) {
return conditionFlags.contains(flagName);
}
public Set<String> getConditionFlags() {
return conditionFlags;
}
}