The only thing remaining is for us to write the EDIFACT formatter. Application OrganizationFigure 5.7 shows the UML model for our application. The major classes are as follows :
EdifactFormatterListing 5.4 is the code for EdifactFormatter . It uses a SAX parser to read the XML document. Listing 5.4 EdifactFormatter.javapackage com.psol.xsledi; import java.io.*; import java.util.*; import org.xml.sax.*; public class EdifactFormatter implements DocumentHandler { protected static final int NONE = 0, MESSAGE = 1, SEGMENT = 2, COMPOSITE_DATA = 3, SEGMENT_SIMPLE_DATA = 4, COMPOSITE_SIMPLE_DATA = 5; protected int state = NONE; protected Segment segment = null; protected CompositeData compositeData = null; protected StringBuffer buffer = null; protected Writer writer; public EdifactFormatter(OutputStream out) throws IOException { writer = new OutputStreamWriter(out,"ISO-8859-1"); } public void setDocumentLocator(Locator locator) { } public void startDocument() throws SAXException { } public void endDocument() throws SAXException { try { writer.flush(); } catch(IOException e) { throw new SAXException(e); } } public void startElement(String name,AttributeList atts) throws SAXException { if(name.equals("Message") && state == NONE) state = MESSAGE; else if(name.equals("Segment") && state == MESSAGE) { state = SEGMENT; String segmentTag = atts.getValue("tag"); if(null != segmentTag) segment = new Segment(segmentTag); else throw new SAXException("Empty 'tag'attribute"); } else if(name.equals("Composite") && state == SEGMENT) { state = COMPOSITE_DATA; compositeData = new CompositeData(); } else if(name.equals("Simple") && state == SEGMENT) { state = SEGMENT_SIMPLE_DATA; buffer = new StringBuffer(); } else if(name.equals("Simple") && state == COMPOSITE_DATA) { state = COMPOSITE_SIMPLE_DATA; buffer = new StringBuffer(); } } public void endElement(String name) throws SAXException { try { if(name.equals("Message") && state == MESSAGE) state = NONE; else if(name.equals("Segment") && state == SEGMENT) { state = MESSAGE; segment.toEdifact(writer); } else if(name.equals("Composite") && state == COMPOSITE_DATA) { state = SEGMENT; segment.add(compositeData); compositeData = null; } else if(name.equals("Simple") && state == SEGMENT_SIMPLE_DATA) { state = SEGMENT; SimpleData sd = new SimpleData(buffer.toString()); segment.add(sd); } else if(name.equals("Simple") && state == COMPOSITE_SIMPLE_DATA) { state = COMPOSITE_DATA; SimpleData sd = new SimpleData(buffer.toString()); compositeData.add(sd); } } catch(IOException e) { throw new SAXException(e); } } public void characters(char[] ch,int start,int len) throws SAXException { if(state == SEGMENT_SIMPLE_DATA state == COMPOSITE_SIMPLE_DATA) buffer.append(ch,start,len); } public void ignorableWhitespace(char[] ch,int start,int len) { } public void processingInstruction(String target,String data) { } } Warning As always, this project focuses on the XML side of things. The EdifactFormatter is limited to the most useful options in the EDIFACT syntax. It leaves out a few, seldom used, options. Most of EdifactFormatter should be familiar, but let's review endElement() in more detail. Note that startElement() is typical and is used to track the current state in the XML document. endElement() also tracks state and create instances of simple data elements, composite data elements, and segments. As soon as it reaches the end of a segment, endElement() calls the toEdifact() method to write the segment in EDIFACT. Then, it discards the segment: public void endElement(String name) throws SAXException { try { if(name.equals("Message") && state == MESSAGE) state = NONE; else if(name.equals("Segment") && state == SEGMENT) { state = MESSAGE; segment.toEdifact(writer); } else if(name.equals("Composite") && state == COMPOSITE_DATA) { state = SEGMENT; segment.add(compositeData); compositeData = null; } else if(name.equals("Simple") && state == SEGMENT_SIMPLE_DATA) { state = SEGMENT; SimpleData sd = new SimpleData(buffer.toString()); segment.add(sd); } else if(name.equals("Simple") && state == COMPOSITE_SIMPLE_DATA) { state = COMPOSITE_DATA; SimpleData sd = new SimpleData(buffer.toString()); compositeData.add(sd); } } catch(IOException e) { throw new SAXException(e); } } SegmentListing 5.5 is Segment.java . The Segment takes a tag in its constructor. It also maintains a list of EdifactElement , which represents simple and composite data elements. Listing 5.5 Segment.javapackage com.psol.xsledi; import java.io.*; import java.util.*; class Segment { protected String tag; protected Vector elements = new Vector(); protected boolean empty = true; public Segment(String tag) { this.tag = tag; } public void add(EdifactElement element) { if(!element.isEmpty()) empty = false; elements.addElement(element); } public EdifactElement elementAt(int i) { return (EdifactElement)elements.elementAt(i); } public int getSize() { return elements.size(); } public void toEdifact(Writer writer) throws IOException { if(empty) return; writer.write(tag); int plus = 1; for(int i = 0;i < getSize();i++) { EdifactElement el = elementAt(i); if(el.isEmpty()) plus++; else { for(int j = 0;j < plus;j++) writer.write('+'); plus = 1; el.toEdifact(writer); } } writer.write('\ ''); } } The most interesting part of Segment.java is probably toEdifact() , the method that takes care of the EDIFACT syntax. As you can see, it loops over the EdifactElement and calls the toEdifact() method: public void toEdifact(Writer writer) throws IOException { if(empty) return; writer.write(tag); int plus = 1; for(int i = 0;i < getSize();i++) { EdifactElement el = elementAt(i); if(el.isEmpty()) plus++; else { for(int j = 0;j < plus;j++) writer.write('+'); plus = 1; el.toEdifact(writer); } } writer.write('\ ''); } The tricky part is to handle compression. For example, if the following is read <Segment tag="NUL"><Simple/><Simple/><Simple>1</Simple></Segment> toEdifact() must insert three plus signs before the third field: NUL+++1' However, simply printing a plus sign before each field is dangerous. Consider the following: <Segment tag="NUL"><Simple>1</Simple><Simple/><Simple/></Segment> Because the last two fields are empty, the compression rule dictates no plus sign should exist for them. Therefore, it would read NUL+1' If toEdifact() printed a plus sign before each field, it would need to backtrack at the end of most segments and erase unnecessary trailing plus signs. Instead, toEdifact() delays writing the plus signs until it reaches a non-empty field. The plus variable tracks how many empty fields were written, which is equal to the number of plus signs to insert before the next non-empty field. Note We are not using the visitor pattern, as we did in Chapter 1, "Lightweight Data Storage," because we don't expect to change the EDIFACT syntax any time soon. Changes in the structure of the message (such as the order of segments or fields) are being dealt with in the style sheet. CompositeData and SimpleDataListing 5.6 is the EdifactElement interface . It defines two methods : isEmpty() , which is used for compression, and toEdifact() , which writes the field. Thanks to EdifactElement , Segment doesn't need to worry about the differences between simple and composite data elements. Listing 5.6 EdifactElement.javapackage com.psol.xsledi; import java.io.*; public interface EdifactElement { public boolean isEmpty(); public void toEdifact(Writer writer) throws IOException; } Listing 5.7 is CompositeData . It maintains a list of SimpleData . Note that its toEdifact() method counts colons like Segment 's toEdifact() counts plus signs. Listing 5.7 CompositeData.javapackage com.psol.xsledi; import java.io.*; import java.util.*; class CompositeData implements EdifactElement { protected Vector simples = new Vector(); protected boolean empty = true; public void add(SimpleData simple) { if(!simple.isEmpty()) empty = false; simples.addElement(simple); } public int getSize() { return simples.size(); } public SimpleData simpleDataAt(int i) { return (SimpleData)simples.elementAt(i); } public boolean isEmpty() { return empty; } public void toEdifact(Writer writer) throws IOException { if(isEmpty()) return; // the empty simple data we have encountered so far // in other words, the number of colons to write // before the next non-empty simple data int colons = 0; for(int i = 0;i < getSize();i++) { SimpleData sd = simpleDataAt(i); if(sd.isEmpty()) colons++; else { for(int j = 0;j < colons;j++) writer.write(':'); colons = 1; sd.toEdifact(writer); } } } } Listing 5.8 is the SimpleData class . As you can see, it's trivial: It stores a string. The most involving part is in the toEdifact() method, which implements a simple algorithm to escape special characters. Note that it also converts the text to uppercase; by default EDIFACT does not use lowercase characters. Listing 5.8 SimpleData.javapackage com.psol.xsledi; import java.io.*; public class SimpleData implements EdifactElement { protected String data; public SimpleData(String data) { this.data = data; } public String getData() { return data; } public boolean isEmpty() { return data.length() == 0; } public void toEdifact(Writer writer) throws IOException { if(isEmpty()) return; for(int i = 0;i < data.length();i++) { char c = data.charAt(i); if(c == '\ '') writer.write("?'"); else if(c == '+') writer.write("?+"); else if(c == ':') writer.write("?:"); else if(c == '?') writer.write("??"); else writer.write(Character.toUpperCase(c)); } } } XML2EdifactThe main() method is in class XML2Edifact , which is reproduced in Listing 5.9. As you can see, it creates an EDIFACT formatter and an XSL processor and links the two together. The Xalan XSL processor can generate its output as SAX events so that the EDIFACT formatter plugs directly into it. With other processors, you might need to explicitly write the XML file and parse. Listing 5.9 XML2Edifact.javapackage com.psol.xsledi; import java.io.*; import java.util.*; import org.xml.sax.*; import org.apache.xalan.xslt.*; public class XML2Edifact { public static void main(String args[]) throws SAXException, IOException { if(args.length < 3) throw new IllegalArgumentException( "Usage is XML2Edifact in.xml xsl.xsl out.edi"); InputStream sin = new FileInputStream(args[0]), sxsl = new FileInputStream(args[1]); OutputStream sout = new FileOutputStream(args[2]); EdifactFormatter formatter = new EdifactFormatter(sout); XSLTProcessor processor = XSLTProcessorFactory.getProcessor(); XSLTInputSource in = new XSLTInputSource(sin), xsl = new XSLTInputSource(sxsl); XSLTResultTarget out = new XSLTResultTarget(formatter); processor.process(in,xsl,out); } } |