View Javadoc

1   /*
2    * Copyright (C) 2007 University of Cambridge
3    *
4    * This file is part of Fosstrak (www.fosstrak.org).
5    *
6    * Fosstrak is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License version 2.1, as published by the Free Software Foundation.
9    *
10   * Fosstrak is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13   * Lesser General Public License for more details.
14   *
15   * You should have received a copy of the GNU Lesser General Public
16   * License along with Fosstrak; if not, write to the Free
17   * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18   * Boston, MA  02110-1301  USA
19   */
20  
21  package org.fosstrak.tdt;
22  
23  import java.io.BufferedReader;
24  import java.io.FileNotFoundException;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.math.BigInteger;
29  import java.net.MalformedURLException;
30  import java.net.URL;
31  import java.net.URLConnection;
32  import java.util.ArrayList;
33  import java.util.HashMap;
34  import java.util.HashSet;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  
41  import javax.xml.bind.JAXBContext;
42  import javax.xml.bind.JAXBElement;
43  import javax.xml.bind.JAXBException;
44  import javax.xml.bind.MarshalException;
45  import javax.xml.bind.Unmarshaller;
46  import javax.xml.bind.ValidationException;
47  import javax.xml.parsers.DocumentBuilder;
48  import javax.xml.parsers.DocumentBuilderFactory;
49  import javax.xml.parsers.ParserConfigurationException;
50  import javax.xml.transform.stream.StreamSource;
51  import javax.xml.xpath.XPath;
52  import javax.xml.xpath.XPathConstants;
53  import javax.xml.xpath.XPathExpressionException;
54  import javax.xml.xpath.XPathFactory;
55  
56  import org.epcglobalinc.tdt.EpcTagDataTranslation;
57  import org.epcglobalinc.tdt.Field;
58  import org.epcglobalinc.tdt.GEPC64;
59  import org.epcglobalinc.tdt.GEPC64Entry;
60  import org.epcglobalinc.tdt.Level;
61  import org.epcglobalinc.tdt.LevelTypeList;
62  import org.epcglobalinc.tdt.ModeList;
63  import org.epcglobalinc.tdt.Option;
64  import org.epcglobalinc.tdt.PadDirectionList;
65  import org.epcglobalinc.tdt.Rule;
66  import org.epcglobalinc.tdt.Scheme;
67  import org.w3c.dom.Document;
68  import org.xml.sax.SAXException;
69  
70  /**
71   * 
72   * This class provides methods for translating an electronic product code (EPC)
73   * between various levels of representation including BINARY, TAG_ENCODING,
74   * PURE_IDENTITY and LEGACY formats. An additional output level ONS_HOSTNAME may
75   * be defined for some coding schemes.
76   * 
77   * @author Mark Harrison [University of Cambridge] - mark.harrison@cantab.net
78   * @author James Brusey
79   * @author Jochen Mader - jochen@pramari.com
80   * @author Christian Floerkemeier
81   */
82  public class TDTEngine {
83  
84  	// --------------------------/
85  	// - Class/Member Variables -/
86  	// --------------------------/
87  
88  	/**
89  	 * prefix_tree_map is a map of levels to prefix trees. Each prefix tree is a
90  	 * Trie structure (see wikipedia) that is useful for quickly finding a
91  	 * matching prefix.
92  	 */
93  	private Map<LevelTypeList, PrefixTree<PrefixMatch>> prefix_tree_map = new HashMap<LevelTypeList, PrefixTree<PrefixMatch>>();
94  
95  	/**
96  	 * HashMap gs1cpi is an associative array providing a lookup between either
97  	 * a GS1 Company Prefix and the corresponding integer-based Company Prefix
98  	 * Index, where one of these has been registered for use with 64-bit EPCs -
99  	 * or the reverse lookup from Company Prefix Index to GS1 Company Prefix.
100 	 * Note that this is an optimization to avoid having to do an xpath trawl
101 	 * through the CPI table each time.
102 	 */
103 	private HashMap<String, String> gs1cpi = new HashMap<String, String>();
104 
105 	/** The gepc64 table xml. */
106 	private String GEPC64xml;
107 
108 	// ----------------/
109 	// - Constructors -/
110 	// ----------------/
111 
112 	/**
113 	 * Legacy constructor for a new Tag Data Translation engine.
114 	 * 
115 	 * @param confdir
116 	 *            the string value of the path to a configuration directory
117 	 *            consisting of two subdirectories, <code>schemes</code> and
118 	 *            <code>auxiliary</code>.
119 	 * 
120 	 *            <p>
121 	 *            When the class TDTEngine is constructed, the path to a local
122 	 *            directory must be specified, by passing it as a single string
123 	 *            parameter to the constructor method, e.g.
124 	 *            <code><pre>TDTEngine engine = new TDTEngine("/opt/TDT");</pre></code>
125 	 *            </p>
126 	 *            <p>
127 	 *            The specified directory must contain two subdirectories named
128 	 *            auxiliary and schemes. The Tag Data Translation definition
129 	 *            files for the various coding schemes should be located inside
130 	 *            the subdirectory called <code>schemes</code>. Any auxiliary
131 	 *            lookup files (such as <code>ManagerTranslation.xml</code>)
132 	 *            should be located inside the subdirectory called
133 	 *            <code>auxiliary</code>.
134 	 * 
135 	 *            Files within the schemes directory ending in <code>.xml</code>
136 	 *            are read in and unmarshalled using JAXB.
137 	 */
138 	@Deprecated
139 	public TDTEngine(String confdir) throws FileNotFoundException,
140 			MarshalException, ValidationException, TDTException {
141 
142 		
143 		try {
144 			Unmarshaller unmar = getUnmarshaller();
145 			URL confdirurl;
146 			if (confdir.endsWith("/")) {
147 				confdirurl = new URL("file","localhost",confdir);
148 			} else {
149 				confdirurl = new URL("file","localhost",confdir+"/");
150 			}
151 
152 			URL scheme = new URL(confdirurl,"schemes/");
153 			URL auxGEPC64table = new URL(confdirurl,"auxiliary/ManagerTranslation.xml");
154 
155 			Set<String> schemes = new HashSet<String>();
156 
157 			URLConnection urlcon = scheme.openConnection();
158 			urlcon.connect();
159 			BufferedReader in = new BufferedReader(new InputStreamReader(urlcon.getInputStream()));
160 			String line;
161 			for (; (line = in.readLine()) != null;) {
162 				if (line.endsWith(".xml")) {
163 					URL defurl = new URL(scheme,line);
164 					loadEpcTagDataTranslation(unmar, defurl);
165 					schemes.add(line);
166 				}
167 			}
168 			loadGEPC64Table(unmar, auxGEPC64table);
169 		} catch (MalformedURLException e) {
170 			throw new FileNotFoundException(e.getMessage());
171 		} catch (IOException e) {
172 			throw new FileNotFoundException(e.getMessage());
173 		} catch (JAXBException e) {
174 			throw new MarshalException(e);
175 		}
176 
177 	}
178 
179 	/**
180 	 * Constructor for a new Tag Data Translation engine. This constructor uses
181 	 * the schemes included on the classpath in a directory called schemes 
182            * (or from within the jar). The ManagerTranslation.xml file is loaded from a directory
183 	* called auxiliary on the classpath. All schemes used need to be listed in a file schemes/schemes.list	   
184 	 * 
185 	 * @throws IOException
186 	 *             thrown if the url is unreachable
187 	 * @throws JAXBException
188 	 *             thrown if the schemes could not be parsed
189 	 */
190 	public TDTEngine() throws IOException, JAXBException {
191 		
192 		Unmarshaller unmar = getUnmarshaller();
193 
194 		URL auxiliary = this.getClass().getClassLoader().getResource(
195 				"auxiliary/ManagerTranslation.xml");
196 
197 		URL schemes = TDTEngine.class.getClassLoader().getResource(
198 				"schemes/schemes.list");
199 
200 		URLConnection urlcon = schemes.openConnection();
201 		urlcon.connect();
202 		BufferedReader in = new BufferedReader(new InputStreamReader(urlcon
203 				.getInputStream()));
204 		String line;
205 		for (; (line = in.readLine()) != null;) {
206 			loadEpcTagDataTranslation(unmar, TDTEngine.class.getClassLoader()
207 					.getResource("schemes/" + line));
208 		}
209 		loadGEPC64Table(unmar, auxiliary);
210 	}
211 
212 	/**
213 	 * Constructor for a new Tag Data Translation engine. All files are
214 	 * unmarshalled using JAXB.
215 	 * 
216 	 * @param auxiliarydir
217 	 *            URL to the directory containing auxiliary files such as the GEPC64Table, 
218 	 *					"ManagerTranslation.xml"
219 	 * @param schemesdir
220 	 *            URL to the directory containing the schemes, all files ending in xml are
221 	 *            read and parsed
222 	 * @throws IOException
223 	 *             thrown if the url is unreachable
224 	 * @throws JAXBException
225 	 *             thrown if the files could not be parsed
226 	 */
227 	public TDTEngine(URL auxiliarydir, URL schemesdir) throws IOException,
228 			JAXBException {
229 		Unmarshaller unmar = getUnmarshaller();
230 
231 		if (!(auxiliarydir.toString().endsWith("/"))) {
232 			// if a trailing slash was missing, append a trailing slash
233 				auxiliarydir = new URL(auxiliarydir.toString()+"/");
234 		}
235 		
236 		if (!(schemesdir.toString().endsWith("/"))) {
237 			// if a trailing slash was missing, append a trailing slash
238 				schemesdir = new URL(schemesdir.toString()+"/");
239 		}
240 		
241 		Set<String> requiredauxfiles = new HashSet<String>();
242 		requiredauxfiles.add("ManagerTranslation.xml");
243 
244 		Set<String> schemes = new HashSet<String>();
245 		
246 		
247 		Set<URL> schemeURLs = getschemeURLs(schemesdir,schemes);
248 		for (URL defurl : schemeURLs) {
249 			loadEpcTagDataTranslation(unmar, defurl);
250 		}
251 		
252 		HashMap<String,URL> auxfileURLs = getauxiliaryURLs(auxiliarydir,requiredauxfiles);
253 	
254 		URL GEPC64table = auxfileURLs.get("ManagerTranslation.xml");
255 		loadGEPC64Table(unmar, GEPC64table);
256 	}
257 
258 
259 
260 	/**
261 	 * Constructor for a new Tag Data Translation engine. All files are
262 	 * unmarshalled using JAXB.
263 	 * 
264 	 * @param auxiliarydir
265 	 *            URL to the directory containing auxiliary files such as the GEPC64Table, 
266 	 *					"ManagerTranslation.xml"
267 	 * @param schemeslist
268 	 *            set containing several urls pointing to directories containing
269 	 *            the schemes. All files ending in xml are read and parsed.
270 	 * @param absolute
271 	 *            true if the given URLs are absolute
272 	 * @throws IOException
273 	 *             thrown if the url is unreachable
274 	 * @throws JAXBException
275 	 *             thrown if the files could not be parsed
276 	 */
277 	public TDTEngine(URL auxiliarydir, Set<URL> schemeslist, boolean absolute) throws JAXBException, IOException {
278 	
279 		Unmarshaller unmar = getUnmarshaller();
280 		Set<String> schemes = new HashSet<String>();
281 		
282 		if (!(auxiliarydir.toString().endsWith("/"))) {
283 			// if a trailing slash was missing, append a trailing slash
284 				auxiliarydir = new URL(auxiliarydir.toString()+"/");
285 		}
286 		
287 		if (absolute) {
288 			for (URL scheme: schemeslist) {
289 				loadEpcTagDataTranslation(unmar, scheme);
290 			}
291 		
292 		
293 		} else {
294 			for (URL schemedir: schemeslist) {
295 
296 				// if the set<URL> schemes are not absolute URLs, then need to check for trailing slashes
297 				if (!(schemedir.toString().endsWith("/"))) {
298 					// if a trailing slash was missing, append a trailing slash
299 						schemedir = new URL(schemedir.toString()+"/");
300 				}
301 								
302 				Set<URL> schemeURLs = getschemeURLs(schemedir,schemes);
303 				for (URL defurl : schemeURLs) {
304 					loadEpcTagDataTranslation(unmar, defurl);
305 				}
306 			}
307 		}
308 
309 
310 		URL GEPC64table = new URL(auxiliarydir, "ManagerTranslation.xml");
311 		loadGEPC64Table(unmar, GEPC64table);
312 	}
313 
314 	/**
315 	 * Creates the unmarshaller.
316 	 * 
317 	 * @return
318 	 * @throws JAXBException
319 	 */
320 	private Unmarshaller getUnmarshaller() throws JAXBException {
321 		JAXBContext context = JAXBContext.newInstance(
322 				EpcTagDataTranslation.class, GEPC64.class, GEPC64Entry.class);
323 		return context.createUnmarshaller();
324 	}
325 
326 	/**
327 	 * Load an xml file from the given url and unmarshal it into an
328 	 * EPCTagDataTranslation.
329 	 * 
330 	 * @param unmar
331 	 * @param schemeUrl
332 	 * @throws IOException
333 	 * @throws JAXBException
334 	 */
335 	private void loadEpcTagDataTranslation(Unmarshaller unmar, URL schemeUrl)
336 			throws IOException, JAXBException {
337 		URLConnection urlcon = schemeUrl.openConnection();
338 		urlcon.connect();
339 		// xml doesn't have enough info for jaxb to figure out
340 		// the
341 		// classname, so we are doing explicit loading
342 		JAXBElement<EpcTagDataTranslation> el = unmar.unmarshal(
343 				new StreamSource(urlcon.getInputStream()),
344 				EpcTagDataTranslation.class);
345 		EpcTagDataTranslation tdt = el.getValue();
346 		initFromTDT(tdt);
347 	}
348 
349 	/**
350 	 * Load an xml file from the given url and unmarshal it into a GEPC64Table.
351 	 * 
352 	 * @param unmar
353 	 * @param auxiliary
354 	 * @throws IOException
355 	 * @throws JAXBException
356 	 */
357 	private void loadGEPC64Table(Unmarshaller unmar, URL auxiliary)
358 			throws IOException, JAXBException {
359 		URLConnection urlcon = auxiliary.openConnection();
360 		urlcon.connect();
361 		// load the GEPC64Table
362 		JAXBElement<GEPC64> el = unmar.unmarshal(new StreamSource(urlcon
363 				.getInputStream()), GEPC64.class);
364 		GEPC64 cpilookup = el.getValue();
365 		for (GEPC64Entry entry : cpilookup.getEntry()) {
366 			String comp = entry.getCompanyPrefix();
367 			String indx = entry.getIndex().toString();
368 			gs1cpi.put(indx, comp);
369 			gs1cpi.put(comp, indx);
370 		}
371 	}
372 	
373 		/**
374 	 * Private method for obtaining a hashmap of URLs for named auxiliary files, whether in a file directory, web directory or web page
375 	 * 
376 	 * @param auxdirURL
377 	 *            URL to the directory containing auxiliary files or web page linking to auxiliary files
378 	 * @param requiredauxfiles
379 	 *            Set of individual auxiliary files to be retrieved 
380 	 * @return a hash map relating the name of the filename and its URL
381 	 *
382 	 * @throws IOException
383 	 *             thrown if the url is unreachable
384 	 */
385     private static HashMap<String,URL> getauxiliaryURLs(URL auxdirURL, Set<String> requiredauxfiles) {
386 		
387 		
388 		HashMap<String, URL> foundauxfiles = new HashMap<String, URL>();
389 		
390 		String inputAuxLine;
391 		
392 		try {
393  			URL parent = new URL(auxdirURL,".");
394 						
395 			String relauxiliary=auxdirURL.getFile();
396 						
397             URLConnection urlconauxiliary = auxdirURL.openConnection();
398             BufferedReader dis2 = new BufferedReader(new InputStreamReader(urlconauxiliary.getInputStream()));
399 			
400             while ((inputAuxLine = dis2.readLine()) != null) {
401 				
402 				for (String requestedfile: requiredauxfiles) {
403 					if (requestedfile.endsWith(".xml")) {
404 						String pattern = requestedfile.replaceAll("\\.","\\\\.").replaceAll("^","(").replaceAll("$",")").toString();
405 						Pattern regex = Pattern.compile(pattern);
406 						
407 						
408 						String pattern2 = requestedfile.replaceAll("\\.","\\\\.").replaceAll("^","href=['\"]([^ ]+?").replaceAll("$",")['\"]").toString();
409 						Pattern regex2 = Pattern.compile(pattern2);
410 						
411 						// check if line includes filename.xml - if so, extract auxiliaryfile           
412 						Matcher matcher2 = regex.matcher(inputAuxLine);
413 						if (matcher2.find()) {
414 							URL relURL = new URL(parent, matcher2.group(1));
415 							foundauxfiles.put(requestedfile, relURL);
416 						}
417 						
418 						// check if line includes href="filename.xml - if so, extract auxiliaryfile           
419 						Matcher matcher3 = regex2.matcher(inputAuxLine);
420 						if (matcher3.find()) {
421 							URL relURL = new URL(parent, matcher3.group(1));
422 							foundauxfiles.put(requestedfile, relURL);
423 						}
424 					}
425 				}	
426 			}
427 			
428 			dis2.close();
429 			
430 			
431         } catch (MalformedURLException me) {
432             System.out.println("MalformedURLException: " + me);
433         } catch (IOException ioe) {
434             System.out.println("IOException: " + ioe);
435         }
436 		return foundauxfiles;    
437 		
438     }
439     
440 	/**
441 	 * Private method for obtaining a set of URLs for TDT definition files, whether in a file directory, web directory or web page
442 	 * 
443 	 * @param schemesdirURL
444 	 *            URL to the directory containing TDT definition files files or web page linking to TDT definition files
445 	 * @return a list of URLs of TDT definition files contained within the directory or web page pointed to by the URL
446 	 *
447 	 * @throws IOException
448 	 *             thrown if the url is unreachable
449 	 */
450     private static Set<URL> getschemeURLs(URL schemesdirURL, Set<String> schemes) {
451         String inputSchemeLine;
452 
453 		Set<URL> schemeURLs = new HashSet<URL>();
454 		try {
455  			URL parent = new URL(schemesdirURL,".");
456 
457 			String relschemes=schemesdirURL.getFile();
458 
459 
460 			
461             URLConnection urlconschemes = schemesdirURL.openConnection();
462             BufferedReader dis = new BufferedReader(new InputStreamReader(urlconschemes.getInputStream()));
463 			
464             while ((inputSchemeLine = dis.readLine()) != null) {
465 				// check if line includes filename.xml - if so, add to Set            
466 				Matcher matcher = Pattern.compile("([A-Za-z0-9-_]+-[0-9*]+\\.xml)").matcher(inputSchemeLine);
467 				if (matcher.find()) {
468 					URL relURL = new URL(parent, matcher.group(0));
469 					if (!matcher.group(0).contains("test")) {
470 					schemeURLs.add(relURL);
471 					schemes.add(matcher.group(0));
472 					}
473 
474  				}
475 			}
476             dis.close();
477         } catch (MalformedURLException me) {
478             System.out.println("MalformedURLException: " + me);
479         } catch (IOException ioe) {
480             System.out.println("IOException: " + ioe);
481         }
482 		
483 		return schemeURLs;
484     }
485 
486 
487 
488 
489 	// -----------/
490 	// - Methods -/
491 	// -----------/
492 
493 	private class PrefixMatch {
494 		private Scheme s;
495 		private Level level;
496 
497 		public PrefixMatch(Scheme s, Level level) {
498 			this.s = s;
499 			this.level = level;
500 		}
501 
502 		public Scheme getScheme() {
503 			return s;
504 		}
505 
506 		public Level getLevel() {
507 			return level;
508 		}
509 	}
510 
511 	/** initialise various indices */
512 	private void initFromTDT(EpcTagDataTranslation tdt) {
513 		for (Scheme ss : tdt.getScheme()) {
514 			// create an index so that we can find a scheme based on tag length
515 
516 			for (Level level : ss.getLevel()) {
517 				String s = level.getPrefixMatch();
518 				if (s != null) {
519 					// insert into prefix tree according to level type.
520 					PrefixTree<PrefixMatch> prefix_tree = prefix_tree_map
521 							.get(level.getType());
522 					if (prefix_tree == null) {
523 						prefix_tree = new PrefixTree<PrefixMatch>();
524 						prefix_tree_map.put(level.getType(), prefix_tree);
525 					}
526 					prefix_tree.insert(s, new PrefixMatch(ss, level));
527 				}
528 			}
529 
530 		}
531 	}
532 
533 	/**
534 	 * Given an input string, and optionally a tag length, find a scheme / level
535 	 * with a matching prefix and tag length.
536 	 */
537 	private PrefixMatch findPrefixMatch(String input, String tagLength) {
538 		List<PrefixMatch> match_list = new ArrayList<PrefixMatch>();
539 
540 		for (PrefixTree<PrefixMatch> tree : prefix_tree_map.values()) {
541 
542 			List<PrefixMatch> list = tree.search(input);
543 
544 			if (!list.isEmpty()) {
545 				if (tagLength == null)
546 					match_list.addAll(list);
547 				else {
548 
549 					for (PrefixMatch match : list) {
550 						if (match.getScheme().getTagLength().equals(tagLength))
551 							match_list.add(match);
552 					}
553 				}
554 			}
555 		}
556 
557 		if (match_list.isEmpty())
558 			throw new TDTException(
559 					"No schemes or levels matched the input value");
560 		else if (match_list.size() > 1)
561 			throw new TDTException(
562 					"More than one scheme/level matched the input value");
563 		else
564 			return match_list.get(0);
565 	}
566 
567 	/**
568 	 * Given an input string, level, and optionally a tag length, find a
569 	 * matching prefix.
570 	 */
571 	private PrefixMatch findPrefixMatch(String input, String tagLength,
572 			LevelTypeList level_type) {
573 		List<PrefixMatch> match_list = new ArrayList<PrefixMatch>();
574 		PrefixTree<PrefixMatch> tree = prefix_tree_map.get(level_type);
575 		assert tree != null;
576 		List<PrefixMatch> list = tree.search(input);
577 		if (!list.isEmpty()) {
578 			if (tagLength == null)
579 				match_list.addAll(list);
580 			else {
581 				for (PrefixMatch match : list)
582 					if (match.getScheme().getTagLength() == tagLength)
583 						match_list.add(match);
584 			}
585 		}
586 		if (match_list.isEmpty())
587 			throw new TDTException(
588 					"No schemes or levels matched the input value");
589 		else if (match_list.size() > 1)
590 			throw new TDTException(
591 					"More than one scheme/level matched the input value");
592 		else
593 			return match_list.get(0);
594 	}
595 
596 	/**
597 	 * Translates the input string of a specified input level to a specified
598 	 * outbound level of the same coding scheme. For example, the input string
599 	 * value may be a tag-encoding URI and the outbound level specified by
600 	 * string outboundlevel may be BINARY, in which case the return value is a
601 	 * binary representation expressed as a string.
602 	 * 
603 	 * <p>
604 	 * Note that this version of the method requires that the user specify the
605 	 * input level, rather than searching for it. However it still automatically
606 	 * finds the scheme used.
607 	 * </p>
608 	 * 
609 	 * @param input
610 	 *            input tag coding
611 	 * @param inputLevel
612 	 *            level such as BINARY, or TAG_ENCODING.
613 	 * @param tagLength
614 	 *            tag length such as VALUE_64 or VALUE_96.
615 	 * @param inputParameters
616 	 *            a map with any additional properties.
617 	 * @param outputLevel
618 	 *            required output level.
619 	 * @return output tag coding
620 	 */
621 	public String convert(String input, LevelTypeList inputLevel,
622 			String tagLength, Map<String, String> inputParameters,
623 			LevelTypeList outputLevel) {
624 
625 		PrefixMatch match = findPrefixMatch(input, tagLength, inputLevel);
626 
627 		return convertLevel(match.getScheme(), match.getLevel(), input,
628 				inputParameters, outputLevel);
629 	}
630 
631 	/**
632 	 * The convert method translates a String input to a specified outbound
633 	 * level of the same coding scheme. For example, the input string value may
634 	 * be a tag-encoding URI and the outbound level specified by string
635 	 * outboundlevel may be BINARY, in which case the return value is a binary
636 	 * representation expressed as a string.
637 	 * 
638 	 * @param input
639 	 *            the identifier to be converted.
640 	 * @param inputParameters
641 	 *            additional parameters which need to be provided because they
642 	 *            cannot always be determined from the input value alone.
643 	 *            Examples include the taglength, companyprefixlength and filter
644 	 *            values.
645 	 * @param outputLevel
646 	 *            the outbound level required for the ouput. Permitted values
647 	 *            include BINARY, TAG_ENCODING, PURE_IDENTITY, LEGACY and
648 	 *            ONS_HOSTNAME.
649 	 * @return the identifier converted to the output level.
650 	 */
651 	public String convert(String input, Map<String, String> inputParameters,
652 			LevelTypeList outputLevel) {
653 
654 		String tagLength = null;
655 		if (inputParameters.containsKey("taglength")) {
656 			// in principle, the user should provide a
657 			// TagLengthList object in the parameter list.
658 			String s = inputParameters.get("taglength");
659 			tagLength = s;
660 		}
661 
662 		PrefixMatch match = findPrefixMatch(input, tagLength);
663 
664 		return convertLevel(match.getScheme(), match.getLevel(), input,
665 				inputParameters, outputLevel);
666 	}
667 
668 	/**
669 	 * convert from a particular scheme / level
670 	 */
671 	private String convertLevel(Scheme tdtscheme, Level tdtlevel, String input,
672 			Map<String, String> inputParameters, LevelTypeList outboundlevel) {
673 
674 		String outboundstring;
675 		Map<String, String> extraparams =
676 		// new NoisyMap
677 		(new HashMap<String, String>(inputParameters));
678 
679 		// get the scheme's option key, which is the name of a
680 		// parameter whose value is matched to the option key of the
681 		// level.
682 
683 		String optionkey = tdtscheme.getOptionKey();
684 		String optionValue = extraparams.get(optionkey);
685 		// the name of a parameter which allows the appropriate option
686 		// to be selected
687 
688 		// now consider the various options within the scheme and
689 		// level for each option element inside the level, check
690 		// whether the pattern attribute matches as a regular
691 		// expression
692 
693 		String matchingOptionKey = null;
694 		Option matchingOption = null;
695 		Matcher prefixMatcher = null;
696 		for (Option opt : tdtlevel.getOption()) {
697 			if (optionValue == null || optionValue.equals(opt.getOptionKey())) {
698 				// possible match
699 
700 				Matcher matcher = Pattern.compile(opt.getPattern()).matcher(
701 						input);
702 				if (matcher.matches()) {
703 					if (prefixMatcher != null)
704 						throw new TDTException("Multiple patterns matched");
705 					prefixMatcher = matcher;
706 					matchingOptionKey = opt.getOptionKey();
707 					matchingOption = opt;
708 				}
709 			}
710 		}
711 		if (prefixMatcher == null)
712 			throw new TDTException("No patterns matched");
713 
714 		optionValue = matchingOptionKey;
715 
716 		for (Field field : matchingOption.getField()) {
717 			BigInteger seq = field.getSeq();
718 			String strfieldname = field.getName();
719 			String strfieldvalue = prefixMatcher.group(seq.intValue());
720 			// System.out.println("   processing field " + strfieldname + " = '"
721 			// + strfieldvalue + "'");
722 
723 			if (field.getCompaction() == null) {
724 				// if compaction is null, treat field as an integer
725 
726 				if (field.getCharacterSet() != null) { // if the character set
727 					// is specified
728 					Matcher charsetmatcher = Pattern.compile(
729 							"^" + field.getCharacterSet() + "$").matcher(
730 							strfieldvalue);
731 					if (!charsetmatcher.matches()) {
732 						throw new TDTException(
733 								"field "
734 										+ strfieldname
735 										+ " ("
736 										+ strfieldvalue
737 										+ ") does not conform to the allowed character set ("
738 										+ field.getCharacterSet() + ") ");
739 					}
740 				}
741 
742 				BigInteger bigvalue = null;
743 
744 				if (tdtlevel.getType() == LevelTypeList.BINARY) { // if the
745 					// input was
746 					// BINARY
747 					bigvalue = new BigInteger(strfieldvalue, 2);
748 					extraparams.put(strfieldname, bigvalue.toString());
749 				} else {
750 					if (field.getDecimalMinimum() != null
751 							|| field.getDecimalMaximum() != null)
752 						bigvalue = new BigInteger(strfieldvalue);
753 					extraparams.put(strfieldname, strfieldvalue);
754 				}
755 
756 				if (field.getDecimalMinimum() != null) { // if the decimal
757 					// minimum is
758 					// specified
759 					BigInteger bigmin = new BigInteger(field
760 							.getDecimalMinimum());
761 
762 					if (bigvalue.compareTo(bigmin) == -1) { // throw an
763 						// exception if the
764 						// field value is
765 						// less than the
766 						// decimal minimum
767 						throw new TDTException("field " + strfieldname + " ("
768 								+ bigvalue + ") is less than DecimalMinimum ("
769 								+ field.getDecimalMinimum() + ") allowed");
770 					}
771 				}
772 
773 				if (field.getDecimalMaximum() != null) { // if the decimal
774 					// maximum is
775 					// specified
776 					BigInteger bigmax = new BigInteger(field
777 							.getDecimalMaximum());
778 
779 					if (bigvalue.compareTo(bigmax) == 1) { // throw an excpetion
780 						// if the field
781 						// value is greater
782 						// than the decimal
783 						// maximum
784 						throw new TDTException("field " + strfieldname + " ("
785 								+ bigvalue
786 								+ ") is greater than DecimalMaximum ("
787 								+ field.getDecimalMaximum() + ") allowed");
788 					}
789 				}
790 
791 				// after extracting the field, it may be necessary to pad it.
792 				padField(extraparams, field);
793 
794 			} else {
795 				// compaction is specified - interpret binary as a string value
796 				// using a truncated byte per character
797 
798 				String compaction = field.getCompaction();
799 				PadDirectionList padDir = field.getPadDir();
800 				String padchar = field.getPadChar();
801 				String s;
802 				if ("5-bit".equals(compaction))
803 					// "5-bit"
804 					s = bin2uppercasefive(strfieldvalue);
805 				else if ("6-bit".equals(compaction))
806 					// 6-bit
807 					s = bin2alphanumsix(strfieldvalue);
808 				else if ("7-bit".equals(compaction))
809 					// 7-bit
810 					s = bin2asciiseven(strfieldvalue);
811 				else if ("8-bit".equals(compaction))
812 					// 8-bit
813 					s = bin2bytestring(strfieldvalue);
814 				else
815 					throw new Error("unsupported compaction method "
816 							+ compaction);
817 				extraparams.put(strfieldname, stripPadChar(s, padDir, padchar));
818 
819 			}
820 
821 		} // for each field;
822 
823 		/**
824 		 * the EXTRACT rules are performed after parsing the input, in order to
825 		 * determine additional fields that are to be derived from the fields
826 		 * obtained by the pattern match process
827 		 */
828 
829 		int seq = 0;
830 		for (Rule tdtrule : tdtlevel.getRule()) {
831 			if (tdtrule.getType() == ModeList.EXTRACT) {
832 				assert seq < tdtrule.getSeq().intValue() : "Rule out of sequence order";
833 				seq = tdtrule.getSeq().intValue();
834 				processRules(extraparams, tdtrule);
835 			}
836 		}
837 
838 		/**
839 		 * Now we need to consider the corresponding output level and output
840 		 * option. The scheme must remain the same, as must the value of
841 		 * optionkey (to select the corresponding option element nested within
842 		 * the required outbound level)
843 		 */
844 
845 		Level tdtoutlevel = findLevel(tdtscheme, outboundlevel);
846 		Option tdtoutoption = findOption(tdtoutlevel, optionValue);
847 
848 		/**
849 		 * the FORMAT rules are performed before formatting the output, in order
850 		 * to determine additional fields that are required for preparation of
851 		 * the outbound format
852 		 */
853 
854 		seq = 0;
855 		for (Rule tdtrule : tdtoutlevel.getRule()) {
856 			if (tdtrule.getType() == ModeList.FORMAT) {
857 				assert seq < tdtrule.getSeq().intValue() : "Rule out of sequence order";
858 				seq = tdtrule.getSeq().intValue();
859 				processRules(extraparams, tdtrule);
860 			}
861 		}
862 
863 		/**
864 		 * Now we need to ensure that all fields required for the outbound
865 		 * grammar are suitably padded etc. processPadding takes care of firstly
866 		 * padding the non-binary fields if padChar and padDir, length are
867 		 * specified then (if necessary) converting to binary and padding the
868 		 * binary representation to the left with zeros if the bit string is has
869 		 * fewer bits than the bitLength attribute specifies. N.B. TDTv1.1 will
870 		 * be more specific about bit-level padding rather than assuming that it
871 		 * is always to the left with the zero bit.
872 		 */
873 
874 		// System.out.println(" prior to processPadding, " + extraparams);
875 		for (Field field : tdtoutoption.getField()) {
876 			// processPadding(extraparams, field, outboundlevel, tdtoutoption);
877 
878 			padField(extraparams, field);
879 			if (outboundlevel == LevelTypeList.BINARY)
880 				binaryPadding(extraparams, field);
881 
882 		}
883 
884 		/**
885 		 * Construct the output from the specified grammar (in ABNF format)
886 		 * together with the field values stored in inputparams
887 		 */
888 
889 		outboundstring = buildGrammar(tdtoutoption.getGrammar(), extraparams);
890 
891 		// System.out.println("final extraparams = " + extraparams);
892 		// System.out.println("returned " + outboundstring);
893 		return outboundstring;
894 	}
895 
896 	/**
897 	 * 
898 	 * Converts a binary string into a large integer (numeric string)
899 	 */
900 	public String bin2dec(String binary) {
901 		BigInteger dec = new BigInteger(binary, 2);
902 		return dec.toString();
903 	}
904 
905 	/**
906 	 * 
907 	 * Converts a large integer (numeric string) to a binary string
908 	 */
909 	public String dec2bin(String decimal) {
910 		// TODO: required?
911 		if (decimal == null) {
912 			decimal = "1";
913 		}
914 		BigInteger bin = new BigInteger(decimal);
915 		return bin.toString(2);
916 	}
917 
918 	/**
919 	 * 
920 	 * Converts a hexadecimal string to a binary string
921 	 */
922 	public String hex2bin(String hex) {
923 		BigInteger bin = new BigInteger(hex.toLowerCase(), 16);
924 		return bin.toString(2);
925 	}
926 
927 	/**
928 	 * 
929 	 * Converts a binary string to a hexadecimal string
930 	 */
931 	public String bin2hex(String binary) {
932 		BigInteger hex = new BigInteger(binary, 2);
933 		return hex.toString(16).toUpperCase();
934 	}
935 
936 	/**
937 	 * Returns a string built using a particular grammar. Single-quotes strings
938 	 * are counted as literal strings, whereas all other strings appearing in
939 	 * the grammar require substitution with the corresponding value from the
940 	 * extraparams hashmap.
941 	 */
942 	private String buildGrammar(String grammar, Map<String, String> extraparams) {
943 		StringBuilder outboundstring = new StringBuilder();
944 		String[] fields = Pattern.compile("\\s+").split(grammar);
945 		for (int i = 0; i < fields.length; i++) {
946 			if (fields[i].substring(0, 1).equals("'")) {
947 				outboundstring.append(fields[i].substring(1,
948 						fields[i].length() - 1));
949 			} else {
950 				outboundstring.append(extraparams.get(fields[i]));
951 			}
952 		}
953 
954 		return outboundstring.toString();
955 	}
956 
957 	/**
958 	 * 
959 	 * Converts the value of a specified fieldname from the extraparams map into
960 	 * binary, either handling it as a large integer or taking into account the
961 	 * compaction of each ASCII byte that is specified in the TDT definition
962 	 * file for that particular field
963 	 */
964 	private String fieldToBinary(Field field, Map<String, String> extraparams) {
965 		// really need an index to find field number given fieldname;
966 
967 		String fieldname = field.getName();
968 		String value = extraparams.get(fieldname);
969 		String compaction = field.getCompaction();
970 
971 		if (compaction == null) {
972 			value = dec2bin(value);
973 		} else {
974 			if ("5-bit".equals(compaction)) {
975 				value = uppercasefive2bin(value);
976 			} else if ("6-bit".equals(compaction)) {
977 				value = alphanumsix2bin(value);
978 			} else if ("7-bit".equals(compaction)) {
979 				value = asciiseven2bin(value);
980 			} else if ("8-bit".equals(compaction)) {
981 				value = bytestring2bin(value);
982 			} else
983 				throw new Error("Unsupported compaction " + compaction);
984 		}
985 
986 		return value;
987 	}
988 
989 	/**
990 	 * pad a value according the field definition.
991 	 */
992 	private void padField(Map<String, String> extraparams, Field field) {
993 		String name = field.getName();
994 		String value = extraparams.get(name);
995 		PadDirectionList padDir = field.getPadDir();
996 		int requiredLength = 0;
997 		if (field.getLength() != null) {
998 			requiredLength = field.getLength().intValue();
999 		}
1000 
1001 		// assert value != null;
1002 		if (value == null)
1003 			return;
1004 
1005 		String padCharString = field.getPadChar();
1006 		// if no pad char specified, don't attempt padding
1007 		if (padCharString == null)
1008 			return;
1009 		assert padCharString.length() > 0;
1010 		char padChar = padCharString.charAt(0);
1011 
1012 		StringBuilder buf = new StringBuilder(requiredLength);
1013 		if (padDir == PadDirectionList.LEFT) {
1014 			for (int i = 0; i < requiredLength - value.length(); i++)
1015 				buf.append(padChar);
1016 			buf.append(value);
1017 		} else if (padDir == PadDirectionList.RIGHT) {
1018 			buf.append(value);
1019 			for (int i = 0; i < requiredLength - value.length(); i++)
1020 				buf.append(padChar);
1021 		}
1022 		assert buf.length() == requiredLength;
1023 		if (requiredLength != value.length()) {
1024 			// System.out.println("    updated " + name + " to '" + buf + "'");
1025 			extraparams.put(name, buf.toString());
1026 		}
1027 		/*
1028 		 * else { StringBuilder mybuf = new StringBuilder(); for (int i = 0; i <
1029 		 * value.length(); i++) { if (i > 0) mybuf.append(',');
1030 		 * mybuf.append('\''); mybuf.append(value.charAt(i));
1031 		 * mybuf.append('\''); }
1032 		 * 
1033 		 * 
1034 		 * System.out.println("    field " + name + " not padded as " +
1035 		 * mybuf.toString() + " is already " + requiredLength +
1036 		 * " characters long"); }
1037 		 */
1038 	}
1039 
1040 	/**
1041 	 * If the outbound level is BINARY, convert the string field to binary, then
1042 	 * pad to the left with the appropriate number of zero bits to reach a
1043 	 * number of bits specified by the bitLength attribute of the TDT definition
1044 	 * file.
1045 	 */
1046 
1047 	private void binaryPadding(Map<String, String> extraparams, Field tdtfield) {
1048 		String fieldname = tdtfield.getName();
1049 		int reqbitlength = tdtfield.getBitLength().intValue();
1050 		String value;
1051 
1052 		String binaryValue = fieldToBinary(tdtfield, extraparams);
1053 		if (binaryValue.length() < reqbitlength) {
1054 			int extraBitLength = reqbitlength - binaryValue.length();
1055 
1056 			StringBuilder zeroPaddedBinaryValue = new StringBuilder("");
1057 			for (int i = 0; i < extraBitLength; i++) {
1058 				zeroPaddedBinaryValue.append("0");
1059 			}
1060 			zeroPaddedBinaryValue.append(binaryValue);
1061 			value = zeroPaddedBinaryValue.toString();
1062 		} else {
1063 			if (binaryValue.length() > reqbitlength)
1064 				throw new TDTException("Binary value [" + binaryValue
1065 						+ "] for field " + fieldname
1066 						+ " exceeds maximum allowed " + reqbitlength
1067 						+ " bits.  Decimal value was "
1068 						+ extraparams.get(fieldname));
1069 
1070 			value = binaryValue;
1071 		}
1072 		extraparams.put(fieldname, value);
1073 
1074 	}
1075 
1076 	/**
1077 	 * Removes leading or trailing characters equal to padchar from the
1078 	 * start/end of the string specified as the first parameter. The second
1079 	 * parameter specified the stripping direction as "LEFT" or "RIGHT" and the
1080 	 * third parameter specifies the character to be stripped.
1081 	 */
1082 	private String stripPadChar(String field, PadDirectionList paddir,
1083 			String padchar) {
1084 		String rv;
1085 		if (paddir == null || padchar == null)
1086 			rv = field;
1087 		else {
1088 			String pattern;
1089 			if (paddir == PadDirectionList.RIGHT)
1090 				pattern = "[" + padchar + "]+$";
1091 			else
1092 				// if (paddir == PadDirectionList.LEFT)
1093 				pattern = "^[" + padchar + "]+";
1094 
1095 			rv = field.replaceAll(pattern, "");
1096 
1097 		}
1098 		return rv;
1099 	}
1100 
1101 	/**
1102 	 * 
1103 	 * Adds additional entries to the extraparams hashmap by processing various
1104 	 * rules defined in the TDT definition files. Typically used for string
1105 	 * processing functions, lookup in tables, calculation of check digits etc.
1106 	 */
1107 	private void processRules(Map<String, String> extraparams, Rule tdtrule) {
1108 		String tdtfunction = tdtrule.getFunction();
1109 		int openbracket = tdtfunction.indexOf("(");
1110 		assert openbracket != -1;
1111 		String params = tdtfunction.substring(openbracket + 1, tdtfunction
1112 				.length() - 1);
1113 		String rulename = tdtfunction.substring(0, openbracket);
1114 		String[] parameter = params.split(",");
1115 		String newfieldname = tdtrule.getNewFieldName();
1116 		// System.out.println(tdtfunction + " " + parameter[0] + " " +
1117 		// extraparams.get(parameter[0]));
1118 		/**
1119 		 * Stores in the hashmap extraparams the value obtained from a lookup in
1120 		 * a specified XML table.
1121 		 * 
1122 		 * The first parameter is the given value already known. This is denoted
1123 		 * as $1 in the corresponding XPath expression
1124 		 * 
1125 		 * The second parameter is the string filename of the table which must
1126 		 * be present in the auxiliary subdirectory
1127 		 * 
1128 		 * The third parameter is the column in which the supplied input value
1129 		 * should be sought
1130 		 * 
1131 		 * The fourth parameter is the column whose value should be read for the
1132 		 * corresponding row, in order to obtain the result of the lookup.
1133 		 * 
1134 		 * The rule in the definition file may contain an XPath expression and a
1135 		 * URL where the table may be obtained.
1136 		 */
1137 		if (rulename.equals("TABLELOOKUP")) {
1138 			// parameter[0] is given value
1139 			// parameter[1] is table
1140 			// parameter[2] is input column supplied
1141 			// parameter[3] is output column required
1142 			assert parameter.length == 4 : "incorrect number of parameters to tablelookup "
1143 					+ params;
1144 			if (parameter[1].equals("tdt64bitcpi")) {
1145 				String s = extraparams.get(parameter[0]);
1146 				assert s != null : tdtfunction + " when " + parameter[0]
1147 						+ " is null";
1148 				String t = gs1cpi.get(s);
1149 				assert t != null : "gs1cpi[" + s + "] is null";
1150 				assert newfieldname != null;
1151 				extraparams.put(newfieldname, t);
1152 				// extraparams.put(newfieldname,
1153 				// gs1cpi.get(extraparams.get(parameter[0])));
1154 			} else { // JPB! the following is untested
1155 				String tdtxpath = tdtrule.getTableXPath();
1156 				String tdttableurl = tdtrule.getTableURL();
1157 				String tdtxpathsub = tdtxpath.replaceAll("\\$1", extraparams
1158 						.get(parameter[0]));
1159 				extraparams.put(newfieldname, xpathlookup(
1160 						"ManagerTranslation.xml", tdtxpathsub));
1161 			}
1162 		}
1163 
1164 		/**
1165 		 * Stores the length of the specified string under the new fieldname
1166 		 * specified by the corresponding rule of the definition file.
1167 		 */
1168 		if (rulename.equals("LENGTH")) {
1169 			assert extraparams.get(parameter[0]) != null : tdtfunction
1170 					+ " when " + parameter[0] + " is null";
1171 			if (extraparams.get(parameter[0]) != null) {
1172 				extraparams.put(newfieldname, Integer.toString(extraparams.get(
1173 						parameter[0]).length()));
1174 			}
1175 		}
1176 
1177 		/**
1178 		 * Stores a GS1 check digit in the extraparams hashmap, keyed under the
1179 		 * new fieldname specified by the corresponding rule of the definition
1180 		 * file.
1181 		 */
1182 		if (rulename.equals("GS1CHECKSUM")) {
1183 			assert extraparams.get(parameter[0]) != null : tdtfunction
1184 					+ " when " + parameter[0] + " is null";
1185 			if (extraparams.get(parameter[0]) != null) {
1186 				extraparams.put(newfieldname, gs1checksum(extraparams
1187 						.get(parameter[0])));
1188 			}
1189 		}
1190 
1191 		/**
1192 		 * Obtains a substring of the string provided as the first parameter. If
1193 		 * only a single second parameter is specified, then this is considered
1194 		 * as the start index and all characters from the start index onwards
1195 		 * are stored in the extraparams hashmap under the key named
1196 		 * 'newfieldname' in the corresponding rule of the definition file. If a
1197 		 * second and third parameter are specified, then the second parameter
1198 		 * is the start index and the third is the length of characters
1199 		 * required. A substring consisting characters from the start index up
1200 		 * to the required length of characters is stored in the extraparams
1201 		 * hashmap, keyed under the new fieldname specified by the corresponding
1202 		 * rule of the defintion file.
1203 		 */
1204 		if (rulename.equals("SUBSTR")) {
1205 			assert extraparams.get(parameter[0]) != null : tdtfunction
1206 					+ " when " + parameter[0] + " is null";
1207 			if (parameter.length == 2) {
1208 				if (extraparams.get(parameter[0]) != null) {
1209 					int start = getIntValue(parameter[1], extraparams);
1210 					if (start >= 0) {
1211 						extraparams.put(newfieldname, extraparams.get(
1212 								parameter[0]).substring(start));
1213 					}
1214 				}
1215 
1216 			}
1217 			if (parameter.length == 3) { // need to check that this variation is
1218 				// correct - c.f. Perl substr
1219 				assert extraparams.get(parameter[0]) != null : tdtfunction
1220 						+ " when " + parameter[0] + " is null";
1221 				if (extraparams.get(parameter[0]) != null) {
1222 					int start = getIntValue(parameter[1], extraparams);
1223 					int end = getIntValue(parameter[2], extraparams);
1224 					if ((start >= 0) && (end >= 0)) {
1225 						extraparams.put(newfieldname, extraparams.get(
1226 								parameter[0]).substring(start, start + end));
1227 					}
1228 				}
1229 
1230 			}
1231 		}
1232 
1233 		/**
1234 		 * Concatenates specified string parameters together. Literal values
1235 		 * must be enclosed within single or double quotes or consist of
1236 		 * unquoted digits. Other unquoted strings are considered as fieldnames
1237 		 * and the corresponding value from the extraparams hashmap are
1238 		 * inserted. The result of the concatenation (and substitution) of the
1239 		 * strings is stored as a new entry in the extraparams hashmap, keyed
1240 		 * under the new fieldname specified by the rule.
1241 		 */
1242 		if (rulename.equals("CONCAT")) {
1243 			StringBuilder buffer = new StringBuilder();
1244 			for (int p1 = 0; p1 < parameter.length; p1++) {
1245 				Matcher matcher = Pattern.compile("\"(.*?)\"|'(.*?)'|[0-9]")
1246 						.matcher(parameter[p1]);
1247 				if (matcher.matches()) {
1248 					buffer.append(parameter[p1]);
1249 				} else {
1250 					assert extraparams.get(parameter[p1]) != null : tdtfunction
1251 							+ " when " + parameter[p1] + " is null";
1252 					if (extraparams.get(parameter[p1]) != null) {
1253 						buffer.append(extraparams.get(parameter[p1]));
1254 					}
1255 				}
1256 
1257 			}
1258 			extraparams.put(newfieldname, buffer.toString());
1259 		}
1260 		
1261 		
1262 
1263 		/**
1264 		 * Adds specified parameters together. Unqouted strings are considered
1265 		 * as fieldnames and the corresponding value from the extraparams hashmap
1266 		 * are used in the calculation. 
1267 		 * The result of the addition is stored as a new entry in the extraparams
1268 		 * hashmap, keyed under the new fieldname specified by the rule.
1269 		 */
1270 		if (rulename.equalsIgnoreCase("add")) {
1271 			assert extraparams.get(parameter[0]) != null : tdtfunction + " when " + parameter[0] + " is null";
1272 
1273 			if ((extraparams.get(parameter[0]) != null) && (parameter[1] != null) && (parameter.length == 2)) {
1274 			
1275 				int initialvalue = getIntValue(parameter[0], extraparams);
1276 				int increment = Integer.parseInt(parameter[1]);
1277 				extraparams.put(newfieldname, Integer.toString(initialvalue+increment));
1278 			}
1279 		}
1280 
1281 
1282 
1283 		/**
1284 		 * Multiplies the specified parameters together. 
1285 		 * Unquoted strings are considered as fieldnames and the corresponding
1286 		 * value from the extraparams hashmap are used in the calculation. 
1287 		 * The result of the multiplication is stored as a new entry in the 
1288 		 * extraparams hashmap, keyed under the new fieldname specified by the rule.
1289 		 */
1290 		if (rulename.equalsIgnoreCase("multiply")) {
1291 			assert extraparams.get(parameter[0]) != null : tdtfunction + " when " + parameter[0] + " is null";
1292 
1293 			if ((extraparams.get(parameter[0]) != null) && (parameter[1] != null) && (parameter.length == 2)) {
1294 			
1295 				int initialvalue = getIntValue(parameter[0], extraparams);
1296 				int factor = Integer.parseInt(parameter[1]);
1297 				extraparams.put(newfieldname, Integer.toString(initialvalue*factor));
1298 			}
1299 		}
1300 
1301 
1302 		/**
1303 		 * Divides the first parameter by the second parameter. 
1304 		 * Unquoted strings are considered as fieldnames and the corresponding 
1305 		 * value from the extraparams hashmap are used in the calculation. 
1306 		 * The result of the division is stored as a new entry in the 
1307 		 * extraparams hashmap, keyed under the new fieldname specified by the rule.
1308 		 */
1309 		if (rulename.equalsIgnoreCase("divide")) {
1310 			assert extraparams.get(parameter[0]) != null : tdtfunction + " when " + parameter[0] + " is null";
1311 
1312 			if ((extraparams.get(parameter[0]) != null) && (parameter[1] != null) && (parameter.length == 2)) {
1313 			
1314 				int initialvalue = getIntValue(parameter[0], extraparams);
1315 				int divisor = Integer.parseInt(parameter[1]);
1316 				extraparams.put(newfieldname, Integer.toString(initialvalue*divisor));
1317 			}
1318 		}
1319 
1320 		/**
1321 		 * Subtracts the second parameter from the first parameter. 
1322 		 * Unquoted strings are considered as fieldnames and the corresponding 
1323 		 * value from the extraparams hashmap are used in the calculation. 
1324 		 * The result of the subtraction is stored as a new entry 
1325 		 * in the extraparams hashmap, keyed under the new fieldname specified
1326 		 * by the rule.
1327 		 */
1328 		if (rulename.equalsIgnoreCase("subtract")) {
1329 			assert extraparams.get(parameter[0]) != null : tdtfunction + " when " + parameter[0] + " is null";
1330 
1331 			if ((extraparams.get(parameter[0]) != null) && (parameter[1] != null) && (parameter.length == 2)) {
1332 			
1333 				int initialvalue = getIntValue(parameter[0], extraparams);
1334 				int decrement = Integer.parseInt(parameter[1]);
1335 				extraparams.put(newfieldname, Integer.toString(initialvalue-decrement));
1336 			}
1337 		}
1338 
1339 
1340 		/**
1341 		 * Returns the remainder after integer division of the first parameter
1342 		 * divided by the second parameter. 
1343 		 * Unquoted strings are considered as fieldnames and the corresponding 
1344 		 * value from the extraparams hashmap are used in the calculation. 
1345 		 * The remainder after integer division is stored as a new entry 
1346 		 * in the extraparams hashmap, keyed under the new fieldname specified
1347 		 * by the rule.
1348 		 */
1349 		if (rulename.equalsIgnoreCase("mod")) {
1350 			assert extraparams.get(parameter[0]) != null : tdtfunction + " when " + parameter[0] + " is null";
1351 
1352 			if ((extraparams.get(parameter[0]) != null) && (parameter[1] != null) && (parameter.length == 2)) {
1353 			
1354 				int initialvalue = getIntValue(parameter[0], extraparams);
1355 				int divisor = Integer.parseInt(parameter[1]);
1356 				extraparams.put(newfieldname, Integer.toString(initialvalue % divisor));
1357 			}
1358 		}
1359 
1360 		
1361 		
1362 	}
1363 
1364 	/**
1365 	 * 
1366 	 * Returns the value of a specified fieldname from the specified hashmap and
1367 	 * returns an integer value or throws an exception if the value is not an
1368 	 * integer
1369 	 */
1370 	private int getIntValue(String fieldname, Map<String, String> extraparams) {
1371 		Matcher checkint = Pattern.compile("^\\d+$").matcher(fieldname);
1372 		int rv;
1373 		if (checkint.matches()) {
1374 			rv = Integer.parseInt(fieldname);
1375 		} else {
1376 			if (extraparams.containsKey(fieldname)) {
1377 				rv = Integer.parseInt(extraparams.get(fieldname));
1378 			} else {
1379 				rv = -1;
1380 				throw new TDTException("No integer value for " + fieldname
1381 						+ " can be found - check extraparams");
1382 			}
1383 		}
1384 		return rv;
1385 	}
1386 
1387 	/**
1388 	 * 
1389 	 * Performs an XPATH lookup in an xml document. The document has been loaded
1390 	 * into a private member. The XPATH expression is supplied as the second
1391 	 * string parameter The return value is of type string e.g. this is
1392 	 * currently used primarily for looking up the Company Prefix Index for
1393 	 * encoding a GS1 Company Prefix into a 64-bit EPC tag
1394 	 * 
1395 	 */
1396 	private String xpathlookup(String xml, String expression) {
1397 
1398 		try {
1399 
1400 			// Parse the XML as a W3C document.
1401 			DocumentBuilder builder = DocumentBuilderFactory.newInstance()
1402 					.newDocumentBuilder();
1403 			Document document = builder.parse(GEPC64xml);
1404 
1405 			XPath xpath = XPathFactory.newInstance().newXPath();
1406 
1407 			String rv = (String) xpath.evaluate(expression, document,
1408 					XPathConstants.STRING);
1409 
1410 			return rv;
1411 
1412 		} catch (ParserConfigurationException e) {
1413 			System.err.println("ParserConfigurationException caught...");
1414 			e.printStackTrace();
1415 			return null;
1416 		} catch (XPathExpressionException e) {
1417 			System.err.println("XPathExpressionException caught...");
1418 			e.printStackTrace();
1419 			return null;
1420 		} catch (SAXException e) {
1421 			System.err.println("SAXException caught...");
1422 			e.printStackTrace();
1423 			return null;
1424 		} catch (IOException e) {
1425 			System.err.println("IOException caught...");
1426 			e.printStackTrace();
1427 			return null;
1428 		}
1429 
1430 	}
1431 
1432 	// auxiliary functions
1433 
1434 	/**
1435 	 * 
1436 	 * Converts a binary string input into a byte string, using 8-bits per
1437 	 * character byte
1438 	 */
1439 	private String bytestring2bin(String bytestring) {
1440 		String binary;
1441 		StringBuilder buffer = new StringBuilder("");
1442 		int len = bytestring.length();
1443 		byte[] bytes = bytestring.getBytes();
1444 		for (int i = 0; i < len; i++) {
1445 			buffer.append(padBinary(dec2bin(Integer.toString(bytes[i])), 8));
1446 		}
1447 		binary = buffer.toString();
1448 		return binary;
1449 	}
1450 
1451 	/**
1452 	 * 
1453 	 * Converts a byte string input into a binary string, using 8-bits per
1454 	 * character byte
1455 	 */
1456 	private String bin2bytestring(String binary) {
1457 		String bytestring;
1458 		StringBuilder buffer = new StringBuilder("");
1459 		int len = binary.length();
1460 		for (int i = 0; i < len; i += 8) {
1461 			int j = Integer.parseInt(bin2dec(padBinary(binary.substring(i,
1462 					i + 8), 8)));
1463 			buffer.append((char) j);
1464 		}
1465 		bytestring = buffer.toString();
1466 		return bytestring;
1467 	}
1468 
1469 	/**
1470 	 * 
1471 	 * Converts an ASCII string input into a binary string, using 7-bit
1472 	 * compaction of each ASCII byte
1473 	 */
1474 	private String asciiseven2bin(String asciiseven) {
1475 		String binary;
1476 		StringBuilder buffer = new StringBuilder("");
1477 		int len = asciiseven.length();
1478 		byte[] bytes = asciiseven.getBytes();
1479 		for (int i = 0; i < len; i++) {
1480 			buffer.append(padBinary(dec2bin(Integer.toString(bytes[i] % 128)),
1481 					8).substring(1, 8));
1482 		}
1483 		binary = buffer.toString();
1484 		return binary;
1485 	}
1486 
1487 	/**
1488 	 * 
1489 	 * Converts a binary string input into an ASCII string output, assuming that
1490 	 * 7-bit compaction was used
1491 	 */
1492 	private String bin2asciiseven(String binary) {
1493 		String asciiseven;
1494 		StringBuilder buffer = new StringBuilder("");
1495 		int len = binary.length();
1496 		for (int i = 0; i < len; i += 7) {
1497 			int j = Integer.parseInt(bin2dec(padBinary(binary.substring(i,
1498 					i + 7), 8)));
1499 			buffer.append((char) j);
1500 		}
1501 		asciiseven = buffer.toString();
1502 		return asciiseven;
1503 	}
1504 
1505 	/**
1506 	 * Converts an alphanumeric string input into a binary string, using 6-bit
1507 	 * compaction of each ASCII byte
1508 	 */
1509 	private String alphanumsix2bin(String alphanumsix) {
1510 		String binary;
1511 		StringBuilder buffer = new StringBuilder("");
1512 		int len = alphanumsix.length();
1513 		byte[] bytes = alphanumsix.getBytes();
1514 		for (int i = 0; i < len; i++) {
1515 			buffer
1516 					.append(padBinary(dec2bin(Integer.toString(bytes[i] % 64)),
1517 							8).substring(2, 8));
1518 		}
1519 		binary = buffer.toString();
1520 		return binary;
1521 	}
1522 
1523 	/**
1524 	 * 
1525 	 * Converts a binary string input into a character string output, assuming
1526 	 * that 6-bit compaction was used
1527 	 */
1528 	private String bin2alphanumsix(String binary) {
1529 		String alphanumsix;
1530 		StringBuilder buffer = new StringBuilder("");
1531 		int len = binary.length();
1532 		for (int i = 0; i < len; i += 6) {
1533 			int j = Integer.parseInt(bin2dec(padBinary(binary.substring(i,
1534 					i + 6), 8)));
1535 			if (j < 32) {
1536 				j += 64;
1537 			}
1538 			buffer.append((char) j);
1539 		}
1540 		alphanumsix = buffer.toString();
1541 		return alphanumsix;
1542 	}
1543 
1544 	/**
1545 	 * Converts an upper case character string input into a binary string, using
1546 	 * 5-bit compaction of each ASCII byte
1547 	 */
1548 	private String uppercasefive2bin(String uppercasefive) {
1549 		String binary;
1550 		StringBuilder buffer = new StringBuilder("");
1551 		int len = uppercasefive.length();
1552 		byte[] bytes = uppercasefive.getBytes();
1553 		for (int i = 0; i < len; i++) {
1554 			buffer
1555 					.append(padBinary(dec2bin(Integer.toString(bytes[i] % 32)),
1556 							8).substring(3, 8));
1557 		}
1558 		binary = buffer.toString();
1559 		return binary;
1560 	}
1561 
1562 	/**
1563 	 * 
1564 	 * Converts a binary string input into a character string output, assuming
1565 	 * that 5-bit compaction was used
1566 	 */
1567 	private String bin2uppercasefive(String binary) {
1568 		String uppercasefive;
1569 		StringBuilder buffer = new StringBuilder("");
1570 		int len = binary.length();
1571 		for (int i = 0; i < len; i += 5) {
1572 			int j = Integer.parseInt(bin2dec(padBinary(binary.substring(i,
1573 					i + 5), 8)));
1574 			buffer.append((char) (j + 64));
1575 		}
1576 		uppercasefive = buffer.toString();
1577 		return uppercasefive;
1578 	}
1579 
1580 	/**
1581 	 * Pads a binary value supplied as a string first parameter to the left with
1582 	 * leading zeros in order to reach a required number of bits, as expressed
1583 	 * by the second parameter, reqlen. Returns a string value corresponding to
1584 	 * the binary value left padded to the required number of bits.
1585 	 */
1586 	private String padBinary(String binary, int reqlen) {
1587 		String rv;
1588 		int l = binary.length();
1589 		int pad = (reqlen - (l % reqlen)) % reqlen;
1590 		StringBuilder buffer = new StringBuilder("");
1591 		for (int i = 0; i < pad; i++) {
1592 			buffer.append("0");
1593 		}
1594 		buffer.append(binary);
1595 		rv = buffer.toString();
1596 		return rv;
1597 	}
1598 
1599 	/**
1600 	 * Calculates the check digit for a supplied input string (assuming that the
1601 	 * check digit will be the digit immediately following the supplied input
1602 	 * string). GS1 (formerly EAN.UCC) check digit calculation methods are used.
1603 	 */
1604 	private String gs1checksum(String input) {
1605 		int checksum;
1606 		int weight;
1607 		int total = 0;
1608 		int len = input.length();
1609 		int d;
1610 		for (int i = 0; i < len; i++) {
1611 			if (i % 2 == 0) {
1612 				weight = -3;
1613 			} else {
1614 				weight = -1;
1615 			}
1616 			d = Integer.parseInt(input.substring(len - 1 - i, len - i));
1617 			total += weight * d;
1618 		}
1619 		checksum = (10 + total % 10) % 10;
1620 		return Integer.toString(checksum);
1621 	}
1622 
1623 	/**
1624 	 * find a level by its type in a scheme. This involves iterating through the
1625 	 * list of levels. The main reason for doing it this way is to avoid being
1626 	 * dependent on the order in which the levels are coded in the xml, which is
1627 	 * not explicitly constrained.
1628 	 */
1629 	private Level findLevel(Scheme scheme, LevelTypeList levelType) {
1630 		Level level = null;
1631 		for (Level lev : scheme.getLevel()) {
1632 			if (lev.getType() == levelType) {
1633 				level = lev;
1634 				break;
1635 			}
1636 		}
1637 		if (level == null)
1638 			throw new Error("Couldn't find type " + levelType + " in scheme "
1639 					+ scheme);
1640 		return level;
1641 	}
1642 
1643 	/**
1644 	 * find a option by its type in a scheme. This involves iterating through
1645 	 * the list of options. The main reason for doing it this way is to avoid
1646 	 * being dependent on the order in which the options are coded in the xml,
1647 	 * which is not explicitly constrained.
1648 	 */
1649 	private Option findOption(Level level, String optionKey) {
1650 		Option option = null;
1651 
1652 		for (Option opt : level.getOption()) {
1653 			if (opt.getOptionKey().equals(optionKey)) {
1654 				option = opt;
1655 				break;
1656 			}
1657 		}
1658 		if (option == null)
1659 			throw new Error("Couldn't find option for " + optionKey
1660 					+ " in level " + level);
1661 
1662 		return option;
1663 	}
1664 	
1665 	/** 
1666 	 * adds a list of global company prefixes (GCPs) to the current list of GCPs.
1667 	 * The list of GCPs is used to convert a GTIN and serial or an SSCC to an 
1668 	 * EPC number when the user does not provide length of the GCP.
1669 	 * 
1670 	 * The method expects the individual GCPs to be on a new line each. It is up 
1671 	 * to the user to determine wher the GCPs are read from (normal file, network, 
1672 	 * onsepc.com)
1673 	 * 
1674 	 *  @param inputstream 
1675 	 *  			a reference to a source of GCPs 
1676 	 * @throws IOException 
1677 	 */
1678 	
1679 	public void addListOfGCPs(InputStream source) throws IOException {
1680 		
1681 		BufferedReader br = new BufferedReader(new InputStreamReader(
1682 	            source, "US-ASCII"));
1683 	    try {
1684 	      String line;
1685 	      while ((line = br.readLine()) != null) {
1686 	        //System.out.println(line);
1687 	      }
1688 	    } finally {
1689 	      br.close();
1690 	    }	   
1691 	}
1692 	
1693 	/**
1694 	 * converts a GTIN and serial number to the pure identity representation of an EPC. 
1695 	 * The method looks up the length of the global company prefix from a list that can 
1696 	 * loaded into the TDT engine.
1697 	 * 
1698 	 *  @params gtin
1699 	 *  @params serial
1700 	 *  @returns pure identity EPC
1701 	 *  
1702 	 */ 
1703 	
1704 	public String convertGTINandSerialToPureIdentityEPC(String gtin, String serial) {
1705 		
1706 		return " ";
1707 		
1708 	}
1709 	
1710 	/**
1711 	 * converts a GTIN and serial number to the pure identity representation of an EPC. 
1712 	 * The length of the global company prefix is provided as a method parameter.
1713 	 * 
1714 	 *  @params gtin
1715 	 *  @params serial
1716 	 *  @params length of global company prefix
1717 	 *  @returns pure identity EPC
1718 	 *  
1719 	 */ 
1720 	
1721 	public String convertGTINandSerialToPureIdentityEPC(String gtin, String serial, int gcpLength) {
1722 		
1723 		return " ";
1724 		
1725 	}
1726 	
1727 	/**
1728 	 * converts a pure identity EPC to gtin and serial. 
1729 	 * 
1730 	 *  @params epc in pure identity format
1731 	 *  @returns List with gtin and serial
1732 	 *  
1733 	 */ 
1734 	
1735 	public List<String> convertPureIdentityEPCToGTINandSerial(String EPC) {
1736 		
1737 		return new ArrayList<String>();
1738 		
1739 	}
1740 	
1741 	
1742 	/**
1743 	 * converts a SSCC to the pure identity representation of an EPC. The method looks up 
1744 	 * the length of the global company prefix from a list that can loaded into the TDT 
1745 	 * engine via the addGCPs  
1746 	 * 
1747 	 *  @params gtin
1748 	 *  @params serial
1749 	 *  @returns pure identity EPC
1750 	 *  
1751 	 */ 
1752 	
1753 	public String convertSSCCToPureIdentityEPC(String sscc) {
1754 		
1755 		return " ";
1756 		
1757 	}
1758 	
1759 	/**
1760 	 * converts a SSCC to the pure identity representation of an EPC. 
1761 	 * The length of the global company prefix is provided as a method parameter.
1762 	 *  @params gtin
1763 	 *  @params serial
1764 	 *  @params length of global company prefix
1765 	 *  @returns pure identity EPC
1766 	 *  
1767 	 */ 
1768 	
1769 	public String convertSSCCToPureIdentityEPC(String sscc, int gcpLength) {
1770 		
1771 		return " ";
1772 		
1773 	}
1774 	
1775 	/**
1776 	 * converts a pure identity EPC to gtin and serial. 
1777 	 * 
1778 	 *  @params epc in pure identity format
1779 	 *  @returns List with gtin and serial
1780 	 *  
1781 	 */ 
1782 	
1783 	public String convertPureIdentityEPCToSSCC(String EPC) {
1784 		
1785 		return " ";
1786 		
1787 	}
1788 	
1789 	
1790 	
1791 	/**
1792 	 * converts a GLN and serial to the pure identity representation of an EPC. The method looks up 
1793 	 * the length of the global company prefix from a list that can loaded into the TDT 
1794 	 * engine. 
1795 	 * 
1796 	 *  @params gtin
1797 	 *  @params serial
1798 	 *  @returns pure identity EPC
1799 	 *  
1800 	 */ 
1801 	
1802 	public String convertGLNandSerialToPureIdentityEPC(String gln, String serial) {
1803 		
1804 		return " ";
1805 		
1806 	}
1807 	
1808 	/**
1809 	 * converts a GLN and serial to the pure identity representation of an EPC.
1810 	 * The length of the global company prefix is provided as a method parameter.
1811 	 *  @params gtin
1812 	 *  @params serial
1813 	 *  @params length of global company prefix
1814 	 *  @returns pure identity EPC
1815 	 *  
1816 	 */ 
1817 	
1818 	public String convertGLNandSerialToPureIdentityEPC(String gln, String serial, int gcpLength) {
1819 		
1820 		return " ";
1821 		
1822 	}
1823 	
1824 	
1825 	
1826 	/**
1827 	 * converts a binary EPC to a pure identity representation. 
1828 	 *  @params binary EPC
1829 	 *  @returns pure identity EPC
1830 	 *  
1831 	 */ 
1832 	
1833 	public String convertBinaryEPCToPureIdentityEPC(String binary) {
1834 		
1835 		return " ";
1836 		
1837 	}
1838 	
1839 	/**
1840 	 * converts a binary EPC in hex notation to a pure identity representation. 
1841 	 *  @params hexadecimal EPC
1842 	 *  @returns pure identity EPC
1843 	 *  
1844 	 */ 
1845 	
1846 	public String convertHexEPCToPureIdentityEPC(String binary) {
1847 		
1848 		return " ";
1849 		
1850 	}
1851 	
1852 
1853 	
1854 }