package com.openMap1.mapper.converters;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.Vector;
import org.eclipse.emf.ecore.EObject;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.openMap1.mapper.core.MapperException;
import com.openMap1.mapper.structures.MapperWrapper;
import com.openMap1.mapper.structures.StructureDefinition;
import com.openMap1.mapper.util.FileUtil;
import com.openMap1.mapper.util.ModelUtil;
import com.openMap1.mapper.util.XMLUtil;
import com.openMap1.mapper.ElementDef;
import com.openMap1.mapper.MappedStructure;
import com.openMap1.mapper.MapperPackage;
import com.openMap1.mapper.MinMult;
/**
* Wrapper class to convert HIPAA X12 messages into an XML form
* convenient for mapping.
*
* @author robert
*
*/
public class HIPAAConverter extends AbstractMapperWrapper implements MapperWrapper{
private StructureDefinition X12Structure;
private ElementDef X12Root;
private Element X12XMLRoot;
private String rootName;
protected boolean tracing() {return true;}
private boolean writeLineContent = true;
//----------------------------------------------------------------------------------------------------
// constructor
//----------------------------------------------------------------------------------------------------
public HIPAAConverter(MappedStructure mappedStructure, Object rootNameObj) throws MapperException
{
super(mappedStructure, rootNameObj);
if (!(rootNameObj instanceof String))
throw new MapperException("Second argument of X12Converter constructor is not a String");
X12Structure = mappedStructure.getStructureDefinition();
rootName = (String)rootNameObj;
X12Root = X12Structure.nameStructure(rootName);
}
//----------------------------------------------------------------------------------------------------
// Small methods in the MapperWrapper interface
//----------------------------------------------------------------------------------------------------
/**
* @return the type of document transformed to and from;
* see static constants in class AbstractMapperWrapper.
*/
public int transformType() {return AbstractMapperWrapper.TEXT_TYPE;}
/**
*
* @return the file extension of the outer document
*/
public String fileExtension() {return "*.txt";}
//----------------------------------------------------------------------------------------------------
// making an X12 xml instance
//----------------------------------------------------------------------------------------------------
/**
* @param X12BarFileObj input stream of the 'vertical bar' form message
* @param rootName name of the root element; equals the message name
* @return X12.XML Document
* @throws MapperException if, for instance, the actual message structure
* does not match the expected structure
*/
public Document transformIn(Object X12FileObj)
throws MapperException
{
if (!(X12FileObj instanceof InputStream))
throw new MapperException("Input for making X12.xml instance is not an InputStream");
InputStream X12File = (InputStream)X12FileObj;
inResultDoc = XMLUtil.makeOutDoc();
X12XMLRoot = XMLUtil.newElement(inResultDoc, rootName);
inResultDoc.appendChild(X12XMLRoot);
Vector<String> lines = removeEmptyLines(FileUtil.getLines(X12File));
ElementDef currentParent = X12Root;
Element currentXMLParent = X12XMLRoot;
for (int i = 0; i < lines.size(); i++)
{
String line = lines.get(i);
trace("\n" + line);
Vector<ElementDef> possibles = possibleNodes(line);
if (possibles.size() ==0) trace("Cannot match line " + line);
else
{
// choose the best ElementDef to hold the line, depending on the current parent
ElementDef chosen = chooseElement(currentParent, possibles);
Element newXMLParent = writeElement(currentParent, chosen, currentXMLParent,line);
currentParent = (ElementDef)chosen.eContainer();
currentXMLParent = newXMLParent;
}
}
return inResultDoc;
}
/**
*
* @param currentParent ElementDef for the node the previous Element was written under
* @param chosen ElementDef chosen to hold the new ElementDef
* @param currentXMLParent XML Element the previous Element was written under
* @return the new XML parent
* @throws MapperException
*/
private Element writeElement(ElementDef currentParent,ElementDef chosen,
Element currentXMLParent, String line)
throws MapperException
{
Element newParent = null;
Element newEl = XMLUtil.newElement(inResultDoc, chosen.getName());
if (writeLineContent) fillLine(chosen,newEl,line,true);
/* if the chosen ElementDef is under the current parent, carry on growing that tree
otherwise, go back up the tree to find the lowest ancestor */
while (!isAncestor(currentParent,chosen))
{
currentParent = (ElementDef)currentParent.eContainer();
currentXMLParent = (Element)currentXMLParent.getParentNode();
}
newParent = attachDescendant(currentParent,chosen,currentXMLParent,newEl);
return newParent;
}
/**
* Fill in the child elements that represent the content of a line in the X12
* @param chosen ElementDef corresponding to a line in the X12
* @param lineEl the Element to hold it
* @param line the line text
* @param writeProblems if true, write messages for any problems detected
* @return false if any problem is detected
*/
private boolean fillLine(ElementDef chosen,Element lineEl,String line, boolean writeProblems)
throws MapperException
{
boolean OK = true;
int childNodes = chosen.getChildElements().size();
String[] fields = new String[childNodes];
// fill in the array of fields from the line
int fieldNo = 0;
StringTokenizer st = new StringTokenizer(line,"*~",true);
st.nextToken(); // strip off the first 'field' which is just the line name
String lastToken = st.nextToken(); // strip off the first '*'
while ((st.hasMoreTokens()) && OK)
{
String token = st.nextToken();
if (token.equals("*"))
{
fieldNo++;
// overflow of the elements allowed for the line
if (fieldNo > childNodes - 1)
{
OK = false;
if (writeProblems) trace("Line '" + line + "' has more than " + childNodes + fields);
}
// two consecutive '*' mean an empty field
else if(lastToken.equals("*")) fields[fieldNo] = "";
}
else if (!(token.equals("~"))) fields[fieldNo] = token;
lastToken = token;
}
// write Elements for the fields
for (int f = 0; f < childNodes; f++) if (OK)
{
String field = fields[f];
ElementDef child = chosen.getChildElements().get(f);
// if the field has content, write it in an Element
if ((field != null) && (!field.equals("")))
{
Element fieldEl = XMLUtil.textElement(inResultDoc, child.getName(),field);
lineEl.appendChild(fieldEl);
}
// if the field has no content but it should have...
else if (child.getMinMultiplicity() == MinMult.ONE)
{
OK = false;
if (writeProblems) trace("Missing field " + (f+1) + " in line " + line);
}
}
return OK;
}
/**
*
* @param ancestor the chosen ElementDef, to hold the new Element , is somewhere below this node
* @param chosen will hold the new element
* @param currentXMLParent XML element corresponding to ancestor
* @param lineEl new XML element to be attached as a descendant
* @return the parent XML element above the new element
* @throws MapperException
*/
private Element attachDescendant(ElementDef ancestor,ElementDef chosen,
Element currentXMLParent,Element lineEl)
throws MapperException
{
// common case; line Element to be attached is a child (direct)
if (getDistance(ancestor, chosen) == 1)
{
currentXMLParent.appendChild(lineEl);
return currentXMLParent;
}
/* line Element is a more remote descendant; fill in the chain in between
* and return its immediate parent */
else if (getDistance(ancestor, chosen) > 1)
{
ElementDef chosenParent = (ElementDef)chosen.eContainer();
Element parentEl = XMLUtil.newElement(inResultDoc, chosenParent.getName());
parentEl.appendChild(lineEl);
/* ignore the return from the recursive call (reduced distance),
* to return the direct parent of the element for the line */
attachDescendant(ancestor, chosenParent,currentXMLParent,parentEl);
return parentEl;
}
else throw new MapperException("Unexpected case at " + ancestor.getName());
}
/**
*
* @param currentParent the parent node that the last line was attached to
* @param possibles ElementDefs which one might attach this line to
* @return the best choice
* @throws MapperException
*/
private ElementDef chooseElement(ElementDef currentParent, Vector<ElementDef> possibles)
throws MapperException
{
ElementDef chosen = possibles.get(0);
// if there is any choice....
if (possibles.size() > 0)
{
// heuristic; shortest distance from current parent
int minDistance = 1000;
for (int i = 0; i < possibles.size(); i++)
{
int distance = getDistance(currentParent, possibles.get(i));
if (distance < minDistance)
{
chosen = possibles.get(i);
minDistance = distance;
}
}
}
return chosen;
}
/**
* @param el1
* @param el2
* @return the number of steps in the traverse from el1 to el2
*/
private int getDistance(ElementDef el1, ElementDef el2) throws MapperException
{
if (isAncestor(el1,el2)) return steps(el1,el2);
else
{
EObject parent = el1.eContainer();
if (parent instanceof ElementDef) return (1 + getDistance((ElementDef)parent,el2));
//this should never happen
else throw new MapperException("No traverse between nodes");
}
}
/**
*
* @param anc
* @param desc
* @return true if anc is an ancestor node of desc
*/
private boolean isAncestor(ElementDef anc, ElementDef desc)
{
if (anc.equals(desc)) return true;
EObject parent = desc.eContainer();
if (parent instanceof ElementDef) return isAncestor(anc, (ElementDef)parent);
return false;
}
/**
* desc is known to be a descendant of anc
* @param anc
* @param desc
* @return the number of steps between them
*/
private int steps(ElementDef anc, ElementDef desc)
{
if (anc.equals(desc)) return 0;
else return (1 + steps(anc,(ElementDef)desc.eContainer()));
}
/**
* Preferred tag names for X12 lines, depending on the qualfier that follows the line name
*/
private String[][] qualifierNodes = {
{"N1","PR","N1_PayerIdentification"},
{"N1","PE","N1_PayeeIdentification"},
};
/**
* @param start start of an X12 line
* @param qualifier qualifier field which comes immediately after the start
* @return preferred tag name for that start and qualifier, if there is one; or null
*/
private String getPreferredTagName(String start,String qualifier)
{
String preferred = null;
for (int i = 0; i < qualifierNodes.length;i++)
{
String[] triple = qualifierNodes[i];
if ((triple[0].equals(start)) && (triple[1].equals(qualifier)))
preferred = triple[2];
}
return preferred;
}
/**
* @param line a line in the X12 file
* @return any nodes in the structure that could hold this line
*/
private Vector<ElementDef> possibleNodes(String line)
{
Vector<ElementDef> possibles = new Vector<ElementDef>();
StringTokenizer st = new StringTokenizer(line,"*");
String start = st.nextToken();
String qualifier = st.nextToken();
String preferredTag = getPreferredTagName(start,qualifier);
for (Iterator<EObject> ie = ModelUtil.getEObjectsUnder(X12Root,MapperPackage.Literals.ELEMENT_DEF).iterator();ie.hasNext();)
{
ElementDef elDef = (ElementDef)ie.next();
StringTokenizer ut = new StringTokenizer(elDef.getName(),"_");
String tagStart = ut.nextToken();
if ((tagStart.equals(start)) && (isLineElement(elDef)))
{
/* If there is a preferred tag name for this X12 line with its
* qualifier, only allow that one; otherwise keep all candidates */
if ((preferredTag == null)||
((preferredTag != null) && (elDef.getName().equals(preferredTag))))
possibles.add(elDef);
}
}
return possibles;
}
/**
* @param el an ElementDef in the structure
* @return true if this ElementDef corresponds to a line in the X12 message -
* i.e. if it has children but its name does not end in 'Loop'
*/
private boolean isLineElement(ElementDef el)
{
return ((el.getChildElements().size() > 0) && (!(el.getName().endsWith("Loop"))));
}
/**
*
* @param lines lines read from an X12 file
* @return the same with empty lines removed
*/
private Vector<String> removeEmptyLines(Vector<String> lines)
{
Vector<String> result = new Vector<String>();
for (int i = 0; i < lines.size(); i++)
if (lines.get(i).length() > 0) result.add(lines.get(i));
return result;
}
//----------------------------------------------------------------------------------------------------
// making an X12 text instance
//----------------------------------------------------------------------------------------------------
/**
* @param X12Root Root element of a X12.xml message or segment
* @return String array of segment text
*/
public String[] transformOut(Element X12Root)
throws MapperException
{
ArrayList<String> barForm = new ArrayList<String>();
String[] result = (String[])barForm.toArray(new String[barForm.size()]);
return result;
}
}