All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.okapi.lib.xliff2.reader.XLIFFReader Maven / Gradle / Ivy

There is a newer version: 1.47.0
Show newest version
/*===========================================================================
  Copyright (C) 2011-2014 by the Okapi Framework contributors
-----------------------------------------------------------------------------
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
===========================================================================*/

package net.sf.okapi.lib.xliff2.reader;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URI;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.sf.okapi.lib.xliff2.Const;
import net.sf.okapi.lib.xliff2.URIParser;
import net.sf.okapi.lib.xliff2.Util;
import net.sf.okapi.lib.xliff2.changeTracking.ChangeTrack;
import net.sf.okapi.lib.xliff2.changeTracking.Item;
import net.sf.okapi.lib.xliff2.changeTracking.Revision;
import net.sf.okapi.lib.xliff2.changeTracking.Revisions;
import net.sf.okapi.lib.xliff2.core.CTag;
import net.sf.okapi.lib.xliff2.core.CanReorder;
import net.sf.okapi.lib.xliff2.core.Directionality;
import net.sf.okapi.lib.xliff2.core.ExtAttribute;
import net.sf.okapi.lib.xliff2.core.ExtAttributes;
import net.sf.okapi.lib.xliff2.core.ExtContent;
import net.sf.okapi.lib.xliff2.core.ExtElement;
import net.sf.okapi.lib.xliff2.core.ExtElements;
import net.sf.okapi.lib.xliff2.core.Fragment;
import net.sf.okapi.lib.xliff2.core.IExtChild;
import net.sf.okapi.lib.xliff2.core.IWithChangeTrack;
import net.sf.okapi.lib.xliff2.core.IWithExtAttributes;
import net.sf.okapi.lib.xliff2.core.IWithInheritedData;
import net.sf.okapi.lib.xliff2.core.IWithMetadata;
import net.sf.okapi.lib.xliff2.core.IWithNotes;
import net.sf.okapi.lib.xliff2.core.IWithValidation;
import net.sf.okapi.lib.xliff2.core.InheritedData;
import net.sf.okapi.lib.xliff2.core.InsingnificantPartData;
import net.sf.okapi.lib.xliff2.core.InsingnificantPartData.InsignificantPartType;
import net.sf.okapi.lib.xliff2.core.MTag;
import net.sf.okapi.lib.xliff2.core.MidFileData;
import net.sf.okapi.lib.xliff2.core.Note;
import net.sf.okapi.lib.xliff2.core.Notes;
import net.sf.okapi.lib.xliff2.core.Part;
import net.sf.okapi.lib.xliff2.core.ProcessingInstruction;
import net.sf.okapi.lib.xliff2.core.Segment;
import net.sf.okapi.lib.xliff2.core.Skeleton;
import net.sf.okapi.lib.xliff2.core.StartFileData;
import net.sf.okapi.lib.xliff2.core.StartGroupData;
import net.sf.okapi.lib.xliff2.core.StartXliffData;
import net.sf.okapi.lib.xliff2.core.Store;
import net.sf.okapi.lib.xliff2.core.Tag;
import net.sf.okapi.lib.xliff2.core.TagType;
import net.sf.okapi.lib.xliff2.core.Tags;
import net.sf.okapi.lib.xliff2.core.TargetState;
import net.sf.okapi.lib.xliff2.core.Unit;
import net.sf.okapi.lib.xliff2.glossary.Definition;
import net.sf.okapi.lib.xliff2.glossary.GlossEntry;
import net.sf.okapi.lib.xliff2.glossary.Glossary;
import net.sf.okapi.lib.xliff2.glossary.Translation;
import net.sf.okapi.lib.xliff2.its.AnnotatorsRef;
import net.sf.okapi.lib.xliff2.its.DataCategories;
import net.sf.okapi.lib.xliff2.its.ITSReader;
import net.sf.okapi.lib.xliff2.its.TermTag;
import net.sf.okapi.lib.xliff2.matches.Match;
import net.sf.okapi.lib.xliff2.matches.Matches;
import net.sf.okapi.lib.xliff2.metadata.IWithMetaGroup;
import net.sf.okapi.lib.xliff2.metadata.Meta;
import net.sf.okapi.lib.xliff2.metadata.MetaGroup;
import net.sf.okapi.lib.xliff2.metadata.MetaGroup.AppliesTo;
import net.sf.okapi.lib.xliff2.metadata.Metadata;
import net.sf.okapi.lib.xliff2.validation.Rule;
import net.sf.okapi.lib.xliff2.validation.Rule.Normalization;
import net.sf.okapi.lib.xliff2.validation.Rule.Type;
import net.sf.okapi.lib.xliff2.validation.Validation;

/**
 * Implements a stream-based and event-driven XLIFF reader.
 */
public class XLIFFReader implements Closeable {

	/**
	 * Performs as little validation as possible.
	 */
	public static final int VALIDATION_MINIMAL = 0x00;
	/**
	 * Performs as much validation as possible with the reader
	 * (which may not be a full validation for the modules, depending on the modules supported).
	 */
	public static final int VALIDATION_MAXIMAL = 0xFF;
	/**
	 * Includes schemas-based validation.
	 */
	public static final int VALIDATION_INCLUDE_SCHEMAS = 0x01;
	/**
	 * Includes validation of the fragment identification values.
	 */
	public static final int VALIDATION_INCLUDE_FRAGIDPREFIX = 0x02;

	private static final String NOTE_NS = Const.NS_XLIFF_CORE20+"_n";
	
	private final Logger logger = LoggerFactory.getLogger(getClass());
	private final SchemaValidator schValidator;
	private final LocationValidator locValidator;
	
	private XMLStreamReader reader;
	private LinkedList queue;
	private StartXliffData docData;
	private StartFileData startFileData;
	private boolean isMidFile;
	private boolean isInFile;
	private Stack inheritedData;
	private Stack groups;
	private Stack uriContext;
	private Stack valContext;
	private URIParser uriParser;
	private boolean checkTargetOrder;
	private Unit unit;
	private List srcIsolated;
	private List trgIsolated;
	private Segment segment;
	private Part ignorable;
	private Stack xmlAttributes; // Format: 'd|p' for xml:space, followed by xml:lang value
	private boolean reportUnsingnificantParts = false;
	private HashMap fileIds;
	private HashMap unitIds;
	private HashMap groupIds;
	private ArrayList subFlowIds;
	private Stack>> specialIds;
	private int fileCount;
	private int warningCount;
	private int fileLevelUnitOrGroupCount;
	private ITSReader itsReader;

	// Internal class used to read the originalData element
	private static class DataElementContent {
		
		String content;
		Directionality dir;
		
		DataElementContent (String content,
			Directionality dir)
		{
			this.content = content;
			this.dir = dir;
		}
	}
	
	/**
	 * Creates a new XLIFFReader object with full validation.
	 */
	public XLIFFReader () {
		this(XLIFFReader.VALIDATION_MAXIMAL, null);
	}
	
	/**
	 * Creates a new XLIFFReader object.
	 * You can choose the type of validation to perform.
	 * @param validation one of the VALIDATION_* constants or a ORed combination.
	 */
	public XLIFFReader (int validation) {
		this(validation, null);
	}
	
	/**
	 * Creates a new XLIFFReader object.
	 * @param validation one of the VALIDATION_* constants or a ORed combination.
	 * @param uriParserToUse URI parser to use when processing references (e.g. value of ref in an <mrk> element).
	 * If it is set to null, a URIParser object that handles default prefixes will be used.
	 */
	public XLIFFReader (int validation,
		URIParser uriParserToUse)
	{
		if ( (validation & VALIDATION_INCLUDE_SCHEMAS) == VALIDATION_INCLUDE_SCHEMAS ) {
			schValidator = new SchemaValidator();
			locValidator = new LocationValidator();
			locValidator.load(getClass().getResourceAsStream("/net/sf/okapi/lib/xliff2/modules.xml"));
		}
		else {
			schValidator = null;
			locValidator = null;
		}
		
		if ( uriParserToUse == null ) uriParser = new URIParser();
		else uriParser = uriParserToUse;
		// Set the validation option
		uriParser.setErrorOnUnknownPrefix((validation & VALIDATION_INCLUDE_FRAGIDPREFIX) == VALIDATION_INCLUDE_FRAGIDPREFIX);
	}
	
	/**
	 * Validates an XLIFF document passed as a File object.
	 * @param file the XLIFF document to validate.
	 */
	public static void validate (File file) {
		validate(null, file, null, null, null);
	}
	
	/**
	 * Validates an XLIFF document passed as a File object.
	 * @param file the XLIFF document to validate.
	 * @param uriParser the URI parser to use when processing references (can be null).
	 */
	public static void validate (File file,
		URIParser uriParser)
	{
		validate(uriParser, file, null, null, null);
	}
	
	/**
	 * Validates an XLIFF document passed as a URI.
	 * @param inputURI the URI to process.
	 * @param uriParser the URI parser to use when processing references (can be null).
	 */
	public static void validate (URI inputURI,
		URIParser uriParser)
	{
		validate(uriParser, null, inputURI, null, null);
	}
	
	/**
	 * Validates an XLIFF document passed as a string.
	 * @param input the content to process.
	 * @param uriParser the URI parser to use when processing references (can be null).
	 */
	public static void validate (String input,
		URIParser uriParser)
	{
		validate(uriParser, null, null, input, null);
	}
	
	/**
	 * Validates an XLIFF document passed as a stream.
	 * @param inputStream the stream to process.
	 * @param uriParser the URI parser to use when processing references (can be null).
	 */
	public static void validate (InputStream inputStream,
		URIParser uriParser)
	{
		validate(uriParser, null, null, null, inputStream);
	}
	
	private static void validate (URIParser uriParser,
		File file,
		URI uri,
		String string,
		InputStream stream)
	{
		try ( XLIFFReader reader = new XLIFFReader(VALIDATION_MAXIMAL, uriParser) ) {
			reader.open(file, uri, string, stream);
			while ( reader.hasNext() ) {
				reader.next();
			}
		}
	}
	
	/**
	 * Opens an XLIFF document by its File object.
	 * @param file the file to process.
	 */
	public void open (File file) {
		open(file, null, null, null);
	}
	
	/**
	 * Opens an XLIFF document by its URI.
	 * @param inputURI the URI to process.
	 */
	public void open (URI inputURI) {
		open(null, inputURI, null, null);
	}
	
	/**
	 * Opens an XLIFF document passed as a string.
	 * @param input the string containing the document to process.
	 */
	public void open (String input) {
		open(null, null, input, null);
	}
	
	/**
	 * Opens an XLIFF document by its stream.
	 * Note that if a schema validation is done, this call will create a temporary copy of the content.
	 * If you can use {@link #open(File)}, {@link #open(URI)} or {@link #open(String)}. 
	 * @param inputStream the input stream to process.
	 */
	public void open (InputStream inputStream) {
		open(null, null, null, inputStream);
	}
	
	/**
	 * Gets the input stream to process, and, if requested, perform the validation.
	 * One and only one of the arguments must not be null.
	 * @param file the File to process (or null).
	 * @param uri the URI to process (or null).
	 * @param string the String holding the content to process (or null).
	 * @param stream the InputStream to open (or null).
	 * @return the input stream to process (after reset if needed).
	 */
	private StreamSource validateAndGetInput (File file,
		URI uri,
		String string,
		InputStream stream)
	{
		StreamSource inputSource = null;
		try {
			if ( file != null ) {
				inputSource = new StreamSource(file); 
			}
			else if ( uri != null ) {
				inputSource = new StreamSource(new BufferedInputStream(uri.toURL().openStream()));
			}
			else if ( string != null ) {
				inputSource = new StreamSource(new StringReader(string));
			}
			else {
				if ( schValidator != null ) {
					// If we get directly a stream, we need to create some kind of copy of the content
					// to be able to process it twice (Note that the validator also closes the stream).
					inputSource = new StreamSource(createByteArrayInputStream(stream));
					
				}
				else { // If we need the stream only once, just use it.
					inputSource = new StreamSource(stream);
				}
			}
		}
		catch ( IOException e ) {
			error("Cannot create input stream from input. "+e.getLocalizedMessage());
		}
	
		if ( schValidator == null ) {
			return inputSource;
		}
		
		// Else: Do the validation
		schValidator.validate(inputSource);
		
		// Then reset the input for processing
		try {
			if ( file != null ) {
				inputSource = new StreamSource(file);
			}
			else if ( uri != null ) {
				inputSource = new StreamSource(new BufferedInputStream(uri.toURL().openStream()));
			}
			else if ( string != null ) {
				inputSource = new StreamSource(new StringReader(string));
			}
			else { // It's our temporary byte array input stream
				// We reset it
				inputSource.getInputStream().reset();
			}
		}
		catch ( IOException e ) {
			error("Cannot reset the input after schema validation. "+e.getLocalizedMessage());
		}
		
		return inputSource;
	}

	/**
	 * Creates a temporary byte array input stream.
	 * The close() method can be called in the validator and has no effect.
	 * @param inputStream the original input stream.
	 * @return the new input stream.
	 */
	private ByteArrayInputStream createByteArrayInputStream (InputStream inputStream) {
		try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
			int nRead;
			byte[] data = new byte[10240];
			while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
				baos.write(data, 0, nRead);
			}
			baos.flush();
			ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
			bais.mark(Integer.MAX_VALUE);
			return bais;
		} catch ( IOException e ) {
			throw new XLIFFReaderException("Cannot create temporary input stream for schema validation.", e);
		}
	}
	
	/**
	 * Opens the document, and start by validating it if requested.
	 * One and only one of the arguments must not be null.
	 * @param file the File to process (or null).
	 * @param uri the URI to process (or null).
	 * @param string the String holding the content to process (or null).
	 * @param stream the InputStream to open (or null).
	 */
	private void open (File file,
		URI uri,
		String string,
		InputStream stream)
	{
		try {
			// Close any previous stream
			close();
			
			// Do the validation if needed and get the input
			StreamSource inputSource = validateAndGetInput(file, uri, string, stream);
			
			// Start the parsing
			XMLInputFactory fact = XMLInputFactory.newInstance();
			//TODO: Revisit the settings to all reporting of CDATA and document-trailing whitespace
			fact.setProperty(XMLInputFactory.IS_COALESCING, true);
			fact.setProperty(XMLInputFactory.SUPPORT_DTD, false);

			reader = fact.createXMLStreamReader(inputSource);
			groups = new Stack<>();
			
			inheritedData = new Stack<>();
			inheritedData.push(new InheritedData());
			
			uriContext = new Stack<>();
			uriContext.push(new URIContext());

			valContext = new Stack<>();
			valContext.push(new Validation());
			
			xmlAttributes = new Stack<>();
			xmlAttributes.push("d"); // Default is false
			
			queue = new LinkedList<>();
			queue.add(new Event(EventType.START_DOCUMENT, null));
		}
		catch ( XMLStreamException e ) {
			error("Cannot open the XLIFF stream. "+e.getMessage());
		}
	}
	
	/**
	 * Closes the document.
	 */
	@Override
	public void close () {
		try {
			if ( reader != null ) {
				reader.close();
				reader = null;
			}
		}
		catch ( XMLStreamException e ) {
			error("Closing error. "+e.getMessage());
		}
	}

	/**
	 * Indicates if there is another event.
	 * @return true if there is another event, false otherwise.
	 */
	public boolean hasNext () {
		try {
			return reader.hasNext();
		}
		catch (XMLStreamException e) {
			error("Reading error. "+e.getMessage());
		}
		return false;
	}
	
	/**
	 * Gets the next filter event.
	 * @return the next filter event.
	 */
	public Event next () {
		if ( queue.isEmpty() ) {
			readNext();
		}
		if ( queue.peek().getType() == EventType.END_DOCUMENT ) {
			// Check stack of preserveWS: it should be 1
			if ( xmlAttributes.size() != 1 ) {
				warning(String.format("Stack for xml:space is at %d instead of 1.", xmlAttributes.size()));
			}
		}
		return queue.poll();
	}
	
	/**
	 * Sets the flag to report or not parts of the documents that are not significant.
	 * For example white space between structural elements.
	 * 

By default those parts are not reported. * @param reportUnsingnificantParts true to report those parts, false to not report them. */ public void setReportUnsingnificantParts (boolean reportUnsingnificantParts) { this.reportUnsingnificantParts = reportUnsingnificantParts; } /** * Reads the next event. */ private void readNext () { MidFileData midFileData = null; StartGroupData startGroupData = null; try { String tmp, nsUri; while ( reader.hasNext() ) { int type = reader.next(); switch ( type ) { case XMLStreamReader.START_DOCUMENT: // This event is not always triggered, so we trigger START_INPUT in open() break; case XMLStreamReader.END_DOCUMENT: queue.add(new Event(EventType.END_DOCUMENT, null)); return; case XMLStreamReader.START_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); pushXMLAttributes(); switch (nsUri) { case Const.NS_XLIFF_CORE20: switch (tmp) { case Const.ELEM_UNIT: if (isMidFile) { // Make sure we always generate a MID_FILE event midFileData = ensureMidFileData(midFileData); isMidFile = false; } processUnit(); return; case Const.ELEM_GROUP: if (isMidFile) { // Make sure we always generate a MID_FILE event midFileData = ensureMidFileData(midFileData); isMidFile = false; } startGroupData = processStartGroup(); // If we have inherited validation rules we attached them now // If the group has its own rules they will overwrite these if (!valContext.peek().isEmpty()) { startGroupData.setValidation(valContext.peek()); } // Return only when we reach the first unit/group within this group. break; case Const.ELEM_FILE: processStartFile(); return; case Const.ELEM_XLIFF: processXliff(); return; case Const.ELEM_SKELETON: processSkeleton(); return; case Const.ELEM_NOTES: if (groups.isEmpty()) { if (isMidFile) { midFileData = ensureMidFileData(midFileData); processNotes(midFileData); } else { // Passed unit/group // No notes after units/groups error("Notes for a must be before its first or ."); } } else { // The notes are for the current start-group if (startGroupData == null) error("Notes for a must be before its first or ."); else processNotes(startGroupData); } break; default: error(String.format("Invalid element found: '%s'", tmp)); break; } break; case Const.NS_XLIFF_METADATA20: if (groups.isEmpty()) { if (isMidFile) { midFileData = ensureMidFileData(midFileData); processMetadata(midFileData); continue; } } else { processMetadata(startGroupData); continue; } // Else: invalid error("Invalid extension and module element " + reader.getName().toString()); break; case Const.NS_XLIFF_TRACKING20: if (groups.isEmpty()) { if (isMidFile) { midFileData = ensureMidFileData(midFileData); processChangeTracking(midFileData); continue; } } else { processChangeTracking(startGroupData); continue; } // Else: invalid error("Invalid extension and module element " + reader.getName().toString()); break; case Const.NS_XLIFF_VALIDATION20: if (groups.isEmpty()) { if (isMidFile) { midFileData = ensureMidFileData(midFileData); processValidation(midFileData); continue; } } else { processValidation(startGroupData); continue; } // Else: invalid error("Invalid extension and module element " + reader.getName().toString()); break; default: // Else: it's an extension or a module supported as an extension if (groups.isEmpty()) { if (isMidFile) { midFileData = ensureMidFileData(midFileData); processExtElement("file", midFileData.getExtElements()); } else if (isInFile) { // End of file error("Extension and module elements of a must be before its first or ."); } else { // Outside a file: in the xliff element error("No element allowed outside a element."); } } else { // Extension or modules for the current group processExtElement("group", startGroupData.getExtElements()); } break; } break; case XMLStreamReader.END_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); popXMLAttributes(); if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { if ( tmp.equals(Const.ELEM_GROUP) ) { // Don't test groups as empty groups are allowed queue.add(new Event(EventType.END_GROUP, null)); groups.pop(); popInheritedData(); uriContext.pop(); // then we pop the validation context valContext.pop(); popSpecialIds(); return; } if ( tmp.equals(Const.ELEM_FILE) ) { if ( fileLevelUnitOrGroupCount < 1 ) { error("There must be at least one or in a ."); } checkSubFlows(); queue.add(new Event(EventType.END_FILE, null)); isInFile = false; popInheritedData(); uriContext.pop(); valContext.pop(); popSpecialIds(); return; } if ( tmp.equals(Const.ELEM_XLIFF) ) { // Check if we have at least one file if ( fileCount < 1 ) { error("There must be at least one per XLIFF document."); } queue.add(new Event(EventType.END_XLIFF, uriContext.peek())); return; } } break; //TODO: insignificant data causes issue for end-group and end-file events case XMLStreamReader.CHARACTERS: case XMLStreamReader.SPACE: if ( reportUnsingnificantParts ) { queue.add(new Event(EventType.INSIGNIFICANT_PART, uriContext.peek(), new InsingnificantPartData(InsignificantPartType.TEXT, reader.getText()))); if ( startGroupData == null ) return; } break; case XMLStreamReader.COMMENT: if ( reportUnsingnificantParts ) { queue.add(new Event(EventType.INSIGNIFICANT_PART, uriContext.peek(), new InsingnificantPartData(InsignificantPartType.COMMENT, ""))); if ( startGroupData == null ) return; } break; case XMLStreamReader.PROCESSING_INSTRUCTION: if ( reportUnsingnificantParts ) { queue.add(new Event(EventType.INSIGNIFICANT_PART, uriContext.peek(), new InsingnificantPartData(InsignificantPartType.PI, ""))); if ( startGroupData == null ) return; } break; } } } catch ( XMLStreamException e ) { error("Reading error. "+e.getMessage()); } } /** * Create a MidFileData object (and initializes it) the given one is null. * @param current the current MidFileData object. * @return the new or existing MidFileData object. */ private MidFileData ensureMidFileData (MidFileData current) { if ( current == null ) { current = new MidFileData(); queue.add(new Event(EventType.MID_FILE, uriContext.peek(), current)); } return current; } private void checkSubFlows () { for ( String id : subFlowIds ) { if ( !unitIds.containsKey(id) ) { error(String.format("There is a reference to a subFlow id='%s' with no corresponding .", id)); } } } private void pushXMLAttributes () { // Default is inherited String newStates = xmlAttributes.peek(); // Update xml:space if present String tmp = reader.getAttributeValue(Const.NS_XML, "space"); if ( !Util.isNoE(tmp) ) { newStates = (tmp.equals("preserve") ? "p" : "d")+newStates.substring(1); } // Update xml:lang is present tmp = reader.getAttributeValue(Const.NS_XML, "lang"); if ( !Util.isNoE(tmp) ) { String msg; if ( (msg = Util.validateLang(tmp)) != null ) { error(String.format("The xml:lang value '%s' is invalid.\n"+msg, tmp)); } newStates = ""+newStates.charAt(0)+tmp; } // Push new states xmlAttributes.push(newStates); } private void popXMLAttributes () { xmlAttributes.pop(); } private void processXliff () { // Get version String version = reader.getAttributeValue("", Const.ATTR_VERSION); cannotBeNullOrEmpty(Const.ATTR_VERSION, version); if ( !version.startsWith("2.") ) { error(String.format("Not a XLIFF 2.x document (version='%s').", version)); } docData = new StartXliffData(version); // Get srcLang String value = reader.getAttributeValue("", Const.ATTR_SRCLANG); cannotBeNullOrEmpty(Const.ATTR_SRCLANG, value); docData.setSourceLanguage(value); // Get the namespaces docData.setExtAttributes(gatherNamespaces(null)); // Get other attributes for ( int i=0; i extension attributes addExtAttribute(docData, i, false); } } fileIds = new HashMap<>(); fileCount = 0; warningCount = 0; isInFile = false; queue.add(new Event(EventType.START_XLIFF, uriContext.peek(), docData)); } /** * Gets the number of warnings. * @return the number of warnings. */ public int getWarningCount () { return warningCount; } private void error (String message) { reportIssue(message, false); } private void warning (String message) { warningCount++; reportIssue(message, true); } private void reportIssue (String message, boolean warningOnly) { String fpart = ""; if ( fileCount > 0 ) { fpart = "Error in "; if ( startFileData != null ) { fpart = fpart + String.format("id='%s'", startFileData.getId()); } else { fpart = String.format("number %d", fileCount); } if ( unit != null ) { fpart += String.format(", id='%s'", unit.getId()); } else if ( !groups.isEmpty() ) { if ( groups.peek() == null ) { fpart += String.format(", level %d", groups.size()); } else { fpart += String.format(", id='%s'", groups.peek()); } } if (( reader != null ) && reader.hasName() ) { QName qn = reader.getName(); if ( qn != null ) { fpart += "\nLast element read: '"+qn.toString()+"'"; } } fpart += ":\n"; } if ( warningOnly ) { logger.warn(fpart+message); } else { throw new XLIFFReaderException(fpart+message); } } private void processStartFile () { fileCount++; fileLevelUnitOrGroupCount = 0; startFileData = new StartFileData(null); isMidFile = true; // In case there is no skeleton isInFile = true; unitIds = new HashMap<>(); // Reset list of used ids for units groupIds = new HashMap<>(); // Reset list of used ids for groups subFlowIds = new ArrayList<>(); specialIds = new Stack<>(); itsReader = new ITSReader(reader); readAndPushInheritedAttributes(startFileData); uriContext.push(uriContext.peek().clone()); valContext.push(new Validation(valContext.peek(), true)); pushSpecialIds(); if ( locValidator != null ) locValidator.reset(); // Get the namespaces startFileData.setExtAttributes(gatherNamespaces(startFileData.getExtAttributes())); // Get other attributes for ( int i=0; i elements.", value)); } else { fileIds.put(value, false); } uriContext.peek().setFileId(value); startFileData.setId(value); break; case Const.ATTR_ORIGINAL: cannotBeEmpty(Const.ATTR_ORIGINAL, value); startFileData.setOriginal(value); break; default: // Invalid attribute in core namespace error(String.format("Invalid attribute '%s'.", locName)); break; } } //TODO ITS ??? else { // Other namespaces than the core -> extension attributes addExtAttribute(startFileData, i, false); } } // Check id if ( startFileData.getId() == null ) { error("The element must have an id."); } // We are done queue.add(new Event(EventType.START_FILE, uriContext.peek(), startFileData)); } private void pushSpecialIds () { HashMap> ids = new HashMap<>(); specialIds.push(ids); } private void popSpecialIds () { specialIds.pop(); } private void checkAndAddSpecialId (String idNs, String id) { // Do not check uniqueness for the Core // Notes should be checked, but they are associated different temporary namespace URI if ( idNs.equals(Const.NS_XLIFF_CORE20) ) return; HashMap> ids = specialIds.peek(); ArrayList list = ids.get(idNs); if ( list != null ) { if ( list.contains(id) ) { // Duplicate: throw an error if ( idNs.equals(NOTE_NS) ) error(String.format("Duplicate id '%s' for a element.", id)); else error(String.format("Duplicate id '%s' for the module or extension '%s'.", id, idNs)); } } else { list = new ArrayList<>(); ids.put(idNs, list); } list.add(id); } private void processSkeleton () throws XMLStreamException { boolean hasHref = false; Skeleton skelData = new Skeleton(); QName qn = new QName(Const.NS_XLIFF_CORE20, Const.ELEM_SKELETON); // Process attributes for ( int i=0; i.", locName)); } } else { error(String.format("Invalid attribute '%s' in .", locName)); } } // Process the content while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: if ( hasHref ) { error("You cannot have both an href attribute and a content in ."); } pushXMLAttributes(); skelData.addChild(readExtElement()); break; case XMLStreamReader.END_ELEMENT: if ( reader.getName().equals(qn) ) { popXMLAttributes(); // Validate the skeleton data if ( hasHref ) { // Has href: skeleton must be empty if (( skelData.getChildren() != null ) && ( !skelData.getChildren().isEmpty() )) { error("You must not have a content in if there is an href attribute."); } } else { // No href: skeleton must not be empty if (( skelData.getChildren() == null ) || ( skelData.getChildren().isEmpty() )) { error("You must have a content in if there is no href attribute."); } } queue.add(new Event(EventType.SKELETON, uriContext.peek(), skelData)); return; } break; case XMLStreamReader.CHARACTERS: case XMLStreamReader.SPACE: skelData.addChild(new ExtContent(reader.getText())); break; case XMLStreamReader.CDATA: skelData.addChild(new ExtContent(reader.getText(), true)); break; case XMLStreamReader.PROCESSING_INSTRUCTION: skelData.addChild(new ProcessingInstruction("")); break; case XMLStreamReader.COMMENT: //TODO: remember comments break; } } } private void readAndPushInheritedAttributes (IWithInheritedData object) { // Create the new context with inherited values InheritedData indat = new InheritedData(inheritedData.peek()); // Get translate if present String tmp = reader.getAttributeValue("", Const.ATTR_TRANSLATE); if ( canBeYesOrNo(Const.ATTR_TRANSLATE, tmp) ) { indat.setTranslate(tmp.equals(Const.VALUE_YES)); } // Get canResegment if present tmp = reader.getAttributeValue("", Const.ATTR_CANRESEGMENT); if ( canBeYesOrNo(Const.ATTR_CANRESEGMENT, tmp) ) { indat.setCanResegment(tmp.equals(Const.VALUE_YES)); } // Get source directionality if present tmp = reader.getAttributeValue("", Const.ATTR_SRCDIR); if ( canBeAutoOrLtrOrRtl(Const.ATTR_SRCDIR, tmp) ) { switch ( tmp ) { case Const.VALUE_AUTO: indat.setSourceDir(Directionality.AUTO); break; case Const.VALUE_LTR: indat.setSourceDir(Directionality.LTR); break; case Const.VALUE_RTL: indat.setSourceDir(Directionality.RTL); break; } } // Get target directionality if present tmp = reader.getAttributeValue("", Const.ATTR_TRGDIR); if ( canBeAutoOrLtrOrRtl(Const.ATTR_TRGDIR, tmp) ) { switch ( tmp ) { case Const.VALUE_AUTO: indat.setTargetDir(Directionality.AUTO); break; case Const.VALUE_LTR: indat.setTargetDir(Directionality.LTR); break; case Const.VALUE_RTL: indat.setTargetDir(Directionality.RTL); break; } } AnnotatorsRef ar = itsReader.readAnnotatorsRef(false, indat.getAnnotatorsRef()); if ( ar != null ) indat.setAnnotatorsRef(ar); object.setInheritableData(indat); inheritedData.push(indat); } private void popInheritedData () { inheritedData.pop(); } /** * Processes the start of a group. When this method returns the event queue is set with the * proper event, but the return to the caller should be done only when the first unit/group * element inside this group is reached (so extensions, modules, notes can be attached to * this start of group). * @return the data for this start of group. */ private StartGroupData processStartGroup () { // Get the Id if present String tmp = reader.getAttributeValue("", Const.ATTR_ID); // Update the current unit/group count if ( groups.isEmpty() ) fileLevelUnitOrGroupCount++; // Push the new group groups.push(tmp); // Process id mustBeValidNmtoken(Const.ATTR_ID, tmp, false); // Check if it's unique if ( groupIds.containsKey(tmp) ) { error(String.format("Duplicated group id value '%s' detected.", tmp)); } else { // Remember the id groupIds.put(tmp, null); } // Push the group context and set the new group id uriContext.push(uriContext.peek().clone()); uriContext.peek().setGroupId(tmp); pushSpecialIds(); valContext.push(new Validation(valContext.peek(), true)); if ( locValidator != null ) locValidator.reset(); StartGroupData sgd = new StartGroupData(tmp); readAndPushInheritedAttributes(sgd); // Get the namespaces sgd.setExtAttributes(gatherNamespaces(sgd.getExtAttributes())); // Process other attributes for ( int i=0; i extension attributes addExtAttribute(sgd, i, false); } } // We are done queue.add(new Event(EventType.START_GROUP, uriContext.peek(), sgd)); return sgd; } private int checkIntegerValue (String name, String value, int min, int max) { if ( value == null ) return -1; if ( value.isEmpty() ) { error(String.format("Empty attribute '%s'", name)); } try { int tmp = Integer.parseInt(value); if (( tmp < min ) || ( tmp > max )) { error(String.format("Invalid value for attribute '%s'", name)); } return tmp; } catch ( NumberFormatException e ) { error(String.format("Invalid syntax for attribute '%s'", name)); } return -1; } private void checkInlineIds (Unit unit) { HashMap ids = new HashMap<>(); // Check the segments and ignorable for ( Part part : unit ) { String id = part.getId(); if ( id != null ) { if ( ids.containsKey(id) ) { error(String.format("The id '%s' is used incorrectly more than once in the unit id='%s'", id, unit.getId())); } else { ids.put(id, -1); } } } // Check the source markers Tags markers = unit.getStore().getSourceTags(); for ( Tag bm : markers ) { if ( bm.getTagType() != TagType.CLOSING ) { String id = bm.getId(); if ( ids.containsKey(id) ) { if ( ids.get(id) == -1 ) { // Already used by segment or ignorable error(String.format("The id '%s' is already used for a segment or an ignorable element in the unit id='%s'", id, unit.getId())); } else { // 0: Already used in the source error(String.format("The id '%s' is already used in the source content of the unit id='%s'", id, unit.getId())); } } else { // Allowed, and will be allowed once in the target ids.put(id, 0); } } } // Check the target markers markers = unit.getStore().getTargetTags(); for ( Tag bm : markers ) { if ( bm.getTagType() != TagType.CLOSING ) { String id = bm.getId(); if ( ids.containsKey(id) ) { switch ( ids.get(id) ) { case 0: // Allowed to re-use once for target (should not re-use again) ids.put(id, 1); continue; case -1: // Used by a segment or ignorable: cannot re-use error(String.format("The id '%s' is already used for a segment or ignorable element in the unit id='%s'", id, unit.getId())); break; default: // 1: Already re-used once error(String.format("The id '%s' exists twice or more in the target content in the unit id='%s'", id, unit.getId())); break; } } else { // Target-only id: should be used only once ids.put(id, 1); } } } } private void checkTargetAndState (Unit unit) { for ( Part part : unit ) { if ( !part.isSegment() ) continue; Segment seg = (Segment)part; if ( seg.getState() != TargetState.INITIAL ) { if ( !seg.hasTarget() ) { warning("The state is not 'initial', but there is no element."); } else if ( !seg.getSource().isEmpty() && seg.getTarget().isEmpty() ) { warning("The state is not 'initial' but the is empty while the is not."); } } } } /** * Throws an exception if the value is null or empty. * @param name name of the attribute. * @param value value being checked. */ private void cannotBeNullOrEmpty (String name, String value) { if ( Util.isNoE(value) ) { error(String.format("Missing or empty attribute '%s'", name)); } } private boolean mustBeValidNmtoken (String name, String value, boolean allowNull) { if ( value == null ) { if ( allowNull ) return false; // OK but no value error(String.format("Missing attribute '%s'", name)); } if ( !Util.isValidNmtoken(value) ) { error(String.format("Value '%s' is not a valid NMTOKEN for '%s'.", value, name)); } return true; // Value available } /** * Gets the directionality from an attribute value. * @param name the name of the attribute. * @param value the value (can be null). * @param defValue the default value to use if the value is null. * @return the directionality for the attribute. * @throws XLIFFReaderException if an error occurs. */ private Directionality getDirectionality (String name, String value, Directionality defValue) { if ( value == null ) return defValue; switch ( value ) { case Const.VALUE_AUTO: return Directionality.AUTO; case Const.VALUE_LTR: return Directionality.LTR; case Const.VALUE_RTL: return Directionality.RTL; } // Else: error error(String.format("Invalid attribute value for '%s' (must be '%s', '%s' or '%s')", name, Const.VALUE_AUTO, Const.VALUE_LTR, Const.VALUE_RTL)); return Directionality.AUTO; // Never used } private boolean getYesOrNo (String name, String value, boolean defValue) { if ( value == null ) return defValue; if ( value.isEmpty() || ( !value.equals(Const.VALUE_YES) && !value.equals(Const.VALUE_NO) )) { error(String.format("Invalid attribute value for '%s' (must be '%s' or '%s')", name, Const.VALUE_YES, Const.VALUE_NO)); } return value.equals(Const.VALUE_YES); } /** * Throws an exception if the value is empty. * @param name name of the attribute. * @param value value to check. * @return true if the value is not null. False for null value. */ private boolean cannotBeEmpty (String name, String value) { if ( value == null ) return false; // Allowed but nothing to set if ( value.isEmpty() ) { error(String.format("Empty attribute '%s'", name)); } return true; } /** * Checks if the given value is "yes" or "no" * @param name the name of the attribute. * @param value the value to check. * @return true if either "yes" or "no" was the value, * false if value was null. * @throws XLIFFReaderException if there is an invalid value. */ private boolean canBeYesOrNo (String name, String value) { if ( value == null ) return false; // Allowed but nothing to set if ( value.isEmpty() || ( !value.equals(Const.VALUE_YES) && !value.equals(Const.VALUE_NO) )) { error(String.format("Invalid attribute value for '%s' (must be '%s' or '%s')", name, Const.VALUE_YES, Const.VALUE_NO)); } return true; } private boolean canBeYesOrNoOrFirstNo (String name, String value) { if ( value == null ) return false; // Allowed but nothing to set if ( value.isEmpty() || ( !value.equals(Const.VALUE_YES) && !value.equals(Const.VALUE_NO) && !value.equals(Const.VALUE_FIRSTNO) )) { error(String.format("Invalid attribute value for '%s' (must be '%s', '%s' or '%s')", name, Const.VALUE_YES, Const.VALUE_NO, Const.VALUE_FIRSTNO)); } return true; } private boolean canBeAutoOrLtrOrRtl (String name, String value) { if ( value == null ) return false; // Allowed but nothing to set switch ( value ) { case Const.VALUE_AUTO: case Const.VALUE_LTR: case Const.VALUE_RTL: return true; } error(String.format("Invalid attribute value for '%s' (must be '%s', '%s' or '%s')", name, Const.VALUE_AUTO, Const.VALUE_LTR, Const.VALUE_RTL)); return false; } private void processUnit () throws XMLStreamException { // Update the current unit/group count if ( groups.isEmpty() ) fileLevelUnitOrGroupCount++; // New unit String tmp = reader.getAttributeValue("", Const.ATTR_ID); mustBeValidNmtoken(Const.ATTR_ID, tmp, false); // Check if it's unique if ( unitIds.containsKey(tmp) ) { error(String.format("Duplicated unit id value '%s' detected.", tmp)); } else { // Remember the id unitIds.put(tmp, null); } // Create the new unit unit = new Unit(tmp); // update whitespace value unit.setPreserveWS(xmlAttributes.peek().startsWith("p")); Map unitODM = null; srcIsolated = new ArrayList<>(); trgIsolated = new ArrayList<>(); // Push the new URI context and set the unit id uriContext.push(uriContext.peek().clone()); uriContext.peek().setUnitId(tmp); valContext.push(new Validation(valContext.peek(), true)); // If we have inherited validation rules we attached them now if ( !valContext.peek().isEmpty() ) { unit.setValidation(valContext.peek()); } pushSpecialIds(); // Get the inherited attributes readAndPushInheritedAttributes(unit); // Get the namespaces unit.setExtAttributes(gatherNamespaces(unit.getExtAttributes())); boolean needsITSFetch = itsReader.readAttributes(unit, unit, inheritedData.peek().getAnnotatorsRef()); // Process other attributes for ( int i=0; i extension attributes addExtAttribute(unit, i, false); } } ExtElements unitExtElems = null; checkTargetOrder = false; String nsUri; boolean hasOneSegment = false; HashMap partIds = new HashMap<>(); boolean inExtensionPoint = true; boolean notesAllowed = true; while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); pushXMLAttributes(); if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { inExtensionPoint = false; switch (tmp) { case Const.ELEM_SEGMENT: hasOneSegment = true; notesAllowed = false; processPart(true, partIds); break; case Const.ELEM_IGNORABLE: notesAllowed = false; processPart(false, partIds); break; case Const.ELEM_ORIGINALDATA: unitODM = processOriginalData(); // unit-level original data break; case Const.ELEM_NOTES: if (notesAllowed) processNotes(unit); else error("Notes for a must be before its first or ."); break; default: error(String.format("Unexpected element '%s' in ", tmp)); break; } } else { // Else: it's an extension or a module if ( inExtensionPoint ) { switch (nsUri) { case Const.NS_ITS: itsReader.readStandOffElements(tmp, unit, inheritedData.peek().getAnnotatorsRef()); popXMLAttributes(); // Pop states for the initial elements break; case Const.NS_XLIFF_MATCHES20: processMatches(); break; case Const.NS_XLIFF_GLOSSARY20: processGlossary(); break; case Const.NS_XLIFF_METADATA20: processMetadata(unit); break; case Const.NS_XLIFF_VALIDATION20: processValidation(unit); break; case Const.NS_XLIFF_TRACKING20: processChangeTracking(unit); break; default: if (locValidator != null) locValidator.reset(); unitExtElems = processExtElement("unit", unitExtElems); break; } } else { error(String.format("Invalid element '%s'. Extensions and modules must come before core elements.", tmp)); } } break; case XMLStreamReader.END_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); popXMLAttributes(); if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { if ( tmp.equals(Const.ELEM_UNIT) ) { // End of this unit // Attach the extension elements if any if ( unitExtElems != null ) { unit.setExtElements(unitExtElems); } // Check that we have at least one segment if ( !hasOneSegment ) { error("No in ."); } if ( needsITSFetch ) { itsReader.fetchUnresolvedITSGroups(unit); } // Verify the inline IDs are unique checkInlineIds(unit); // Copy the potential original data stored outside copyOriginalDataToCodes(unit.getStore(), unitODM); // Check attributes consistency in opening/closing codes checkPairsConsistency(unit.getStore().getSourceTags()); checkPairsConsistency(unit.getStore().getTargetTags()); checkSourceTargetCorrespondence(unit.getStore().getSourceTags(), unit.getStore().getTargetTags()); checkTargetOrder(unit); checkIsolatedAttributes(unit.getStore().getSourceTags(), srcIsolated); checkIsolatedAttributes(unit.getStore().getTargetTags(), trgIsolated); checkMatchReferences(); checkGlossaryReferences(); checkRevisions(unit); checkTargetAndState(unit); try { Util.validateCopyOf(unit); unit.verifyOpeningsBeforeClosings(false); unit.verifyOpeningsBeforeClosings(true); // Check the markers re-ordering between source and target if ( unit.doNonEmptySourcesHaveNonEmptyTargets() ) { Util.verifyReordering(unit.getOrderedCTags(false), unit.getOrderedCTags(true), true); } else { // Check for missing firstNo (without doing comparison) Util.createFixedSequences(unit.getOrderedCTags(false), true); Util.createFixedSequences(unit.getOrderedCTags(true), true); } unit.verifyReadOnlyTags(); } catch ( Exception e ) { error(e.getMessage()); } // Push to the queue queue.add(new Event(EventType.TEXT_UNIT, uriContext.peek(), unit)); popInheritedData(); uriContext.pop(); valContext.pop(); popSpecialIds(); unit = null; return; } } break; } } } private void checkRevisions (IWithChangeTrack parent) { if ( !parent.hasChangeTrack() ) return; // Else: check the content for ( Revisions revs : unit.getChangeTrack() ) { String appliesTo = revs.getAppliesTo(); if ( appliesTo == null ) { error(String.format("The element '%s' must have a '%s' attribute.", Revisions.TAG_NAME, Revisions.APPLIES_TO_ATTR_NAME)); } String ref = revs.getRef(); if ( ref != null ) { //TODO } } } private void checkMatchReferences () { if ( !unit.hasMatch() ) return; for ( Match match : unit.getMatches() ) { String ref = match.getRef(); if ( unit.getSourceOrTargetReference(ref) == null ) { error(String.format("The ref value '%s' for does not point to an existing span in it parent .", ref)); } } } private void checkGlossaryReferences () { if ( !unit.hasGlossEntry() ) return; // The reference is allowed to be null for glossary entries for ( GlossEntry entry : unit.getGlossary() ) { String ref = entry.getRef(); if (( ref != null ) && ( unit.getSourceOrTargetReference(ref) == null )) { error(String.format("The ref value '%s' for does not point to an existing span in it parent .", ref)); } for ( Translation trans : entry ) { ref = trans.getRef(); if (( ref != null ) && ( unit.getSourceOrTargetReference(ref) == null )) { error(String.format("The ref value '%s' for does not point to an existing span in it parent .", ref)); } } } } private void checkSourceTargetCorrespondence (Tags srcTags, Tags trgTags) { if ( srcTags == null ) return; if ( trgTags == null ) return; for ( Tag stag : srcTags ) { // Search for two tags with same ID String id = stag.getId(); Tag ttag = trgTags.get(id, stag.getTagType()); if ( ttag == null ) { // Maybe an error for ( Tag tag : trgTags ) { if ( tag.getId().equals(id) ) { error(String.format("The codes id='%s' are of different tag-types in source and target.", id)); } } } // If we get here, that means a tag with the same ID does not exist in the target if ( ttag == null ) continue; // Otherwise: // if it is a code, we have already checked the tag-type // we just have to look at the marker case if ( stag.isMarker() ) { MTag smtag = (MTag)stag; MTag tmtag = (MTag)ttag; if ( !Util.equals(smtag.getTranslate(), tmtag.getTranslate()) ) { error(String.format("The translate value is different between source and target marker id='%s'.", id)); } if ( !Util.equals(smtag.getType(), tmtag.getType()) ) { error(String.format("The translate value is different between source and target marker id='%s'.", id)); } // Not checking the value attribute: It could change in target // Not checking the ref attribute: It could change in target } } } private void checkPairsConsistency (Tags tags) { // Start/end markers must have the same canCopy/canDelete/canReorder/canOverlap if ( tags == null ) return; for ( Tag tag : tags ) { if ( tag.getTagType() != TagType.OPENING ) continue; if ( tag.isMarker() ) { // Start marker Tag em = tags.getClosingTag(tag); if ( em == null ) { error(String.format("The for id='%s' has no corresponding .", tag.getId())); } continue; // No other checks for markers } CTag ctag = (CTag)tag; CTag closing = (CTag)tags.getClosingTag(ctag); if ( closing == null ) continue; // Isolated opening //--- start of checks normally not needed // For now we keep these checks // They should not been needed as the fields come from the same code-common object // but we keep this for now, to check if the code-common object is the same if ( closing.getCanOverlap() != ctag.getCanOverlap() ) { error(String.format("The and for id='%s' must have the same canOverlap value.", ctag.getId())); } if ( closing.getCanCopy() != ctag.getCanCopy() ) { error(String.format("The and for id='%s' must have the same canCopy value.", ctag.getId())); } if ( closing.getCanDelete() != ctag.getCanDelete() ) { error(String.format("The and for id='%s' must have the same canDelete value.", ctag.getId())); } //-- end of checks normally not needed if ( ctag.getCanReorder() == CanReorder.FIRSTNO ) { if ( closing.getCanReorder() != CanReorder.NO ) { error(String.format("The for id='%s' must be set to canReorder='no'.", ctag.getId())); } } else if ( closing.getCanReorder() != ctag.getCanReorder() ) { error(String.format("The for id='%s' must have the same canReorder value as its corresponding .", ctag.getId())); } } } private void checkIsolatedAttributes (Tags markers, List list) { for ( Tag bm : markers ) { if ( bm.isMarker() ) continue; switch ( bm.getTagType() ) { case OPENING: if ( markers.getClosingTag(bm) == null ) { // No match found: this should have an isolated flag if ( !list.contains(bm) ) { error(String.format("Missing isolated='yes' for opening code id='%s'.", bm.getId())); } } else { // Not isolated if ( list.contains(bm) ) { error(String.format("Invalid isolated='yes' for opening code id='%s'.", bm.getId())); } } break; case CLOSING: // This part should really not be triggered as reading the id/startRef will trigger an error before. if ( markers.getOpeningTag(bm) == null ) { // No match found: this should have an isolated flag if ( !list.contains(bm) ) { error(String.format("Missing isolated='yes' for closing code id='%s'.", bm.getId())); } } else { // Not isolated if ( list.contains(bm) ) { error(String.format("Invalid isolated='yes' for closing code id='%s'.", bm.getId())); } } break; case STANDALONE: // Nothing to check } } } private void checkTargetOrder (Unit unit) { if ( checkTargetOrder ) { // Do it only if it's needed int max = unit.getPartCount(); int i = 0; int order2; for ( Part part : unit ) { int order1 = part.getTargetOrder(); if ( order1 == 0 ) order1 = i+1; // Default if (( order1 < 1 ) || ( order1 > max )) { error(String.format("Invalid target order '%d'.", order1)); } // Check for duplicates for ( int j=0; jDuring the normal lifetime of an object, the original data are with each code. * This transfer from the store to the code are done only when reading. * @param originalDataMap the map to copy from. */ private void copyOriginalDataToCodes (Store store, Map originalDataMap) { if ( store == null ) return; // Transfer in the source if ( store.hasSourceTag() ) { for ( Tag marker : store.getSourceTags() ) { if ( marker.isMarker() ) continue; // Not a code CTag code = (CTag)marker; String nid = code.getDataRef(); if ( nid != null ) { if ( originalDataMap == null ) { error(String.format("The code id='%s' refers to the id='%s', but no is declared.", code.getId(), nid)); } if ( !originalDataMap.containsKey(nid) ) { error(String.format("No original data found for the id '%s'.", nid)); } code.setData(originalDataMap.get(nid).content); code.setDataDir(originalDataMap.get(nid).dir); } } } // Transfer in the target if ( store.hasTargetTag() ) { for ( Tag marker : store.getTargetTags() ) { if ( marker.isMarker() ) continue; // Not a code CTag code = (CTag)marker; String nid = code.getDataRef(); if ( nid != null ) { if ( originalDataMap == null ) { error(String.format("The code id='%s' refers to the id='%s', but no is declared.", code.getId(), nid)); } if ( !originalDataMap.containsKey(nid) ) { error(String.format("No original data found for the id '%s'.", nid)); } code.setData(originalDataMap.get(nid).content); code.setDataDir(originalDataMap.get(nid).dir); } } } } private void processNote (IWithNotes parent) throws XMLStreamException { Note note = new Note(); // Get the namespaces note.setExtAttributes(gatherNamespaces(null)); for ( int i=0; i -1) note.setPriority(num); // Else default is set already break; case Const.ATTR_CATEGORY: cannotBeEmpty(Const.ATTR_CATEGORY, value); note.setCategory(value); break; default: // Invalid attribute in core namespace error(String.format("Invalid attribute '%s' in .", locName)); break; } } else { // Other namespaces than the core -> extension attributes addExtAttribute(note, i, false); } } StringBuilder sb = new StringBuilder(); while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.CHARACTERS: sb.append(reader.getText()); break; case XMLStreamReader.START_ELEMENT: error("The element has only text content."); case XMLStreamReader.END_ELEMENT: // Can only be the end of the popXMLAttributes(); note.setText(sb.toString()); parent.addNote(note); return; } } } private void processNotes (IWithNotes parent) throws XMLStreamException { // This creates the Notes object for the parent Notes notes = parent.getNotes(); boolean hasNotes = false; // Gather namespaces notes.setExtAttributes(gatherNamespaces(null)); // No attributes on notes if ( reader.getAttributeCount() > 0 ) { error("No attributes are allowed on the element."); } String tmp, nsUri; while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: pushXMLAttributes(); tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { if ( tmp.equals(Const.ELEM_NOTE) ) { // End of this notes element processNote(parent); hasNotes = true; } else { error("Only elements are allowed in elements."); } } else { error("Only elements are allowed in elements."); } break; case XMLStreamReader.END_ELEMENT: popXMLAttributes(); tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { if ( tmp.equals(Const.ELEM_NOTES) ) { // End of this notes element if ( !hasNotes ) { error("A element must have at least one ."); } return; } } break; } } } private Map processOriginalData () throws XMLStreamException { String tmp, nsUri; Map map = new LinkedHashMap<>(); StringBuilder content = new StringBuilder(); String id = null; Directionality dir = null; boolean inData = false; while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.CHARACTERS: if ( id != null ) { content.append(reader.getText()); } break; case XMLStreamReader.START_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); pushXMLAttributes(); if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { if ( tmp.equals(Const.ELEM_DATA) ) { // Get the id id = reader.getAttributeValue("", Const.ATTR_ID); mustBeValidNmtoken(Const.ATTR_ID, id, false); if ( map.containsKey(id) ) { error(String.format("Duplicated id '%s' in original data table.", id)); } // Get the directionality dir = getDirectionality(Const.ATTR_DIR, reader.getAttributeValue("", Const.ATTR_DIR), Directionality.AUTO); inData = true; } else if ( tmp.equals(Const.ELEM_CP) ) { tmp = reader.getAttributeValue("", Const.ATTR_HEX); cannotBeNullOrEmpty(Const.ATTR_HEX, tmp); content.append(convertHexAttribute(tmp)); } else { if ( inData ) error("Only text is allowed in elements."); else error("Only is allowed in elements."); } } else { if ( inData ) error("Only text is allowed in elements."); else error("Only is allowed in elements."); } break; case XMLStreamReader.END_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); popXMLAttributes(); if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { if ( tmp.equals(Const.ELEM_DATA) ) { map.put(id, new DataElementContent(content.toString(), dir)); // Reset values for next data element id = null; dir = null; content.setLength(0); inData = false; } else if ( tmp.equals(Const.ELEM_ORIGINALDATA) ) { // Do we have at least one data element if ( map.isEmpty() ) { error("There must be at least one in a ."); } return map; } // Else: could be end of ELEM_CP: nothing to do } break; } } return null; } private void processPart (boolean isSegment, HashMap partIds) throws XMLStreamException { String tmp, nsUri; Part part; if ( isSegment ) { segment = unit.appendSegment(); part = segment; // Set the defaults for inheritable data segment.setCanResegment(inheritedData.peek().getCanResegment()); for ( int i=0; i has a subState, state must be set explicitly."); } segment.setSubState(value); break; default: error(String.format("Invalid attribute '%s' in .", locName)); break; } } else { error(String.format("Invalid attribute '%s' in .", locName)); } } } else { ignorable = unit.appendIgnorable(); part = ignorable; } // Get id if present tmp = reader.getAttributeValue("", Const.ATTR_ID); if ( mustBeValidNmtoken(Const.ATTR_ID, tmp, true) ) { if ( partIds.containsKey(tmp) ) { error(String.format( "The id value '%s' is used more than once for or .", tmp)); } else { partIds.put(tmp, false); } part.setId(tmp); } Boolean srcPreserveWS = null; Boolean trgPreserveWS = null; while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); pushXMLAttributes(); if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { if ( tmp.equals(Const.ELEM_SOURCE) ) { if ( srcPreserveWS != null ) { error("Cannot have more than one per or ."); } srcPreserveWS = xmlAttributes.peek().startsWith("p"); if ( isSegment ) processContent(segment, false, false); else processContent(ignorable, false, false); } else if ( tmp.equals(Const.ELEM_TARGET) ) { if ( trgPreserveWS != null ) { error("Cannot have more than one per or ."); } trgPreserveWS = xmlAttributes.peek().startsWith("p"); if ( isSegment ) processContent(segment, true, false); else processContent(ignorable, true, false); if ( docData.getTargetLanguage() == null ) { error("No target language defined in a file with a target entry."); } } else { error("Only and are allowed in or elements."); } } else { error("Only and are allowed in or elements."); } break; case XMLStreamReader.END_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); popXMLAttributes(); if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { // Check if we have at least a source if ( srcPreserveWS == null ) { error("Missing element."); } // Verify that both source and target are set to the same whitespace handling option if (( srcPreserveWS != null ) && ( trgPreserveWS != null )) { if ( !srcPreserveWS.equals(trgPreserveWS) ) { error("Source and target must be set to the same whitespace handling option."); } } // We are done return; } break; } } } private void processContent (Part partToFill, boolean isTarget, boolean allowAnyTargetLang) throws XMLStreamException { Fragment frag = new Fragment(partToFill.getStore(), isTarget); String tmp; CTag code = null; boolean inTextContent = true; String dataRef = null; Stack pairs = new Stack<>(); // Set the context for the directionality Stack dirCtx = new Stack<>(); dirCtx.push(isTarget ? partToFill.getStore().getTargetDir() : partToFill.getStore().getSourceDir() ); // Verify the language String lang = xmlAttributes.peek().substring(1); if ( !lang.isEmpty() ) { if ( isTarget ) { if ( docData.getTargetLanguage() == null ) { error("You must define a target language (trgLang) in the element."); } if ( !allowAnyTargetLang && !lang.equals(docData.getTargetLanguage()) ) { error(String.format("Invalid target language ('%s') set or inherited. It should be '%s'.", lang, docData.getTargetLanguage())); } } else if ( !lang.equals(docData.getSourceLanguage()) ) { error(String.format("Invalid source language ('%s') set or inherited. It should be '%s'.", lang, docData.getSourceLanguage())); } } // Else: the value is inherited from srcLang and trgLang and therefore OK. // Update the white space state // The check that source and target are the same for this is done later partToFill.setPreserveWS(xmlAttributes.peek().startsWith("p")); for ( int i=0; i.", locName)); else error(String.format("Invalid attribute '%s' .", locName)); } } else { if ( ns.equals(Const.NS_XML) ) { switch ( locName ) { case "space": case "lang": // Already processed continue; } } // Else: no extension attributes if ( isTarget ) error(String.format("Invalid attribute '%s' in .", locName)); else error(String.format("Invalid attribute '%s' .", locName)); } } String currentElem = null; while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.CHARACTERS: if ( inTextContent ) { frag.append(reader.getText()); } else { error(String.format( "The <%s> element must be empty.", currentElem)); } break; case XMLStreamReader.COMMENT: case XMLStreamReader.PROCESSING_INSTRUCTION: // Ignored and stripped out break; case XMLStreamReader.START_ELEMENT: currentElem = tmp = reader.getLocalName(); String id = reader.getAttributeValue("", Const.ATTR_ID); if ( reader.getNamespaceURI().equals(Const.NS_XLIFF_CORE20) ) { // Element switch (tmp) { case Const.ELEM_OPENINGCODE: mustBeValidNmtoken(Const.ATTR_ID, id, false); code = frag.append(TagType.OPENING, id, null, true); dataRef = setDataRef(code); inTextContent = false; setOtherInlineAttributes(Fragment.CODE_OPENING, code, null, false, isTarget, false, null, dirCtx); break; // Element case Const.ELEM_CLOSINGCODE: String isolated = reader.getAttributeValue("", Const.ATTR_ISOLATED); String startRef = reader.getAttributeValue("", Const.ATTR_STARTREF); if ((id != null) && (startRef != null)) { error("You cannot have both id and startRef attributes on a element."); } if (isolated != null) { canBeYesOrNo(Const.ATTR_ISOLATED, isolated); mustBeValidNmtoken(Const.ATTR_ID, id, false); } else { mustBeValidNmtoken(Const.ATTR_STARTREF, startRef, false); id = startRef; } code = frag.append(TagType.CLOSING, id, null, true); // id = id or startRef dataRef = setDataRef(code); inTextContent = false; setOtherInlineAttributes(Fragment.CODE_CLOSING, code, null, false, isTarget, (isolated != null), (CTag) frag.getOpeningTag(code), dirCtx); break; // Element case Const.ELEM_PLACEHOLDER: mustBeValidNmtoken(Const.ATTR_ID, id, false); code = frag.appendCode(id, null); dataRef = setDataRef(code); inTextContent = false; setOtherInlineAttributes(Fragment.CODE_STANDALONE, code, null, false, isTarget, false, null, dirCtx); break; // Element case Const.ELEM_PAIREDCODES: { mustBeValidNmtoken(Const.ATTR_ID, id, false); code = frag.append(TagType.OPENING, id, null, false); setOtherInlineAttributes(Fragment.CODE_OPENING, code, null, true, isTarget, false, null, dirCtx); // Check for dataRefStart tmp = reader.getAttributeValue("", Const.ATTR_DATAREFSTART); if (cannotBeEmpty(Const.ATTR_DATAREF, tmp)) { dataRef = tmp; code.setDataRef(dataRef); } code.setInitialWithData((dataRef != null)); code = null; // We nullify the code // Closing code CTag closing = new CTag(TagType.CLOSING, id, null); setOtherInlineAttributes(Fragment.CODE_CLOSING, closing, null, true, isTarget, false, null, dirCtx); // Check for dataRefEnd tmp = reader.getAttributeValue("", Const.ATTR_DATAREFEND); if (((tmp != null) && (dataRef == null)) || ((tmp == null) && (dataRef != null))) { // If we have dataRefEnd, we should have dataRefStart and vice-versa warning(String.format("Both '%s' and '%s' should be present or absent.", Const.ATTR_DATAREFSTART, Const.ATTR_DATAREFEND)); } if (cannotBeEmpty(Const.ATTR_DATAREF, tmp)) { closing.setDataRef(tmp); } closing.setInitialWithData((dataRef != null)); dataRef = null; pairs.push(closing); break; } // Element case Const.ELEM_OPENINGANNO: { mustBeValidNmtoken(Const.ATTR_ID, id, false); MTag ann = new MTag(id, null); // Make sure we re-assign the marker as it may have been switched ann = (MTag) setOtherInlineAttributes(Fragment.MARKER_OPENING, null, ann, false, isTarget, false, null, dirCtx); frag.append(ann); break; } // Element case Const.ELEM_CLOSINGANNO: { id = reader.getAttributeValue("", Const.ATTR_STARTREF); mustBeValidNmtoken(Const.ATTR_STARTREF, id, false); MTag ann = frag.closeMarkerSpan(id); setOtherInlineAttributes(Fragment.MARKER_CLOSING, null, ann, false, isTarget, false, null, dirCtx); break; } // Element case Const.ELEM_PAIREDANNO: { mustBeValidNmtoken(Const.ATTR_ID, id, false); MTag ann = new MTag(id, MTag.TYPE_DEFAULT); // Make sure we re-assign the marker as it may have been switched ann = (MTag) setOtherInlineAttributes(Fragment.MARKER_OPENING, null, ann, true, isTarget, false, null, dirCtx); frag.append(ann); // Closing marker MTag closing = new MTag(ann); setOtherInlineAttributes(Fragment.MARKER_CLOSING, null, closing, true, isTarget, false, null, dirCtx); pairs.push(closing); break; } // Element case Const.ELEM_CP: readCP(inTextContent, frag); break; default: // Invalid element error(String.format("Invalid element in inline content: '%s'", reader.getName().toString())); break; } } else { // Not the core namespace error(String.format("Invalid element in inline content: '%s'", reader.getName().toString())); } break; case XMLStreamReader.END_ELEMENT: tmp = reader.getLocalName(); if ( !reader.getNamespaceURI().equals(Const.NS_XLIFF_CORE20) ) { error("Only the core namespace is allowed in content."); } switch (tmp) { case Const.ELEM_OPENINGCODE: case Const.ELEM_CLOSINGCODE: case Const.ELEM_PLACEHOLDER: code.setInitialWithData((dataRef != null)); inTextContent = true; // Back to text content dataRef = null; break; case Const.ELEM_PAIREDCODES: frag.append(pairs.pop()); break; case Const.ELEM_PAIREDANNO: frag.append(pairs.pop()); break; case Const.ELEM_SOURCE: partToFill.setSource(frag); popXMLAttributes(); return; case Const.ELEM_TARGET: partToFill.setTarget(frag); popXMLAttributes(); return; } break; } } } /** * Checks the constraints for an annotation. * @param marker the marker to check. * @param expliciteTranslate true if the marker had the translate attribute set explicitly. */ private void checkAnnotation (MTag marker, boolean expliciteTranslate) { // If we have a ref, check the general syntax String ref = marker.getRef(); URIContext ctx = null; if ( ref != null ) { ctx = uriContext.peek(); uriParser.setURL(ref, ctx.getFileId(), ctx.getGroupId(), ctx.getUnitId()); } // Specific checks for the comment annotations switch ( marker.getType() ) { case "comment": // Check mrk type='comment': either value or ref needs to be set // We do not check if we have both ref and value at the same time as is not explicitly forbidden String val = marker.getValue(); if (( val == null ) && ( ref == null )) { error("A comment annotation must have value or ref specified."); } if (( val != null ) && ( ref != null )) { error("A comment annotation must use either the value or the ref attribute, not both."); } if ( ref != null ) { // If there is a ref attribute it must point to a note inside the unit String nid = uriParser.getNoteId(); if ( nid == null ) { error(String.format("The ref value of a comment annotation must be a note, but '%s' is not.", ref)); } uriParser.complementReference(); // Convert to an absolute reference to check the reference container (it should be a unit). if ( !uriParser.getUnitId().equals(ctx.getUnitId()) || !uriParser.getFileId().equals(ctx.getFileId()) || ( uriParser.getRefContainer() != 'u' )) { error(String.format("The ref value of a comment annotation must be a note in the same unit, but '%s' is not.", ref)); } // If we get here: the syntax is OK boolean found = false; if ( unit.getNoteCount() > 0 ) { for ( Note note : unit.getNotes() ) { if ( note.getId().equals(nid) ) { found = true; break; } } } if ( !found ) { error(String.format("No note with id='%s' found in the unit for '%s'.", nid, ref)); } } break; case "generic": // translate must be present: that is check when reading if ( !expliciteTranslate ) { error("Annotation of type='generic' must have the translate attribute set explicitly."); } break; } } private void readCP (boolean inTextContent, Fragment frag) { if ( !inTextContent ) { error(String.format( "The <%s> element must be empty.", Const.ELEM_CP)); } String tmp = reader.getAttributeValue("", Const.ATTR_HEX); cannotBeNullOrEmpty(Const.ATTR_HEX, tmp); char[] chars = convertHexAttribute(tmp); for ( char c : chars ) { frag.append(c); } } private char[] convertHexAttribute (String value) { try { int cp = Integer.valueOf(value, 16); if ( Util.isValidInXML(cp) ) { // Value is in the range of valid characters: it should not use cp error(String.format("Code-point U+%04X is valid in XML and must not be encoded with .", cp)); } // Else: invalid and therefore OK return Character.toChars(cp); } catch ( IllegalArgumentException e ) { // This catches also NumberFormatException error(String.format("Invalid code-point value in '%s': '%s'", Const.ATTR_HEX, value)); } return null; } private void checkECValueAgainstSC (String attName, String scValue, String ecValue) { if ( scValue == null ) { if ( ecValue != null ) { error(String.format("The value for '%s' is not defined in the element, but is defined in the element.", attName)); } } else if ( !scValue.equals(ecValue) ) { error(String.format("The value '%s' for '%s' in is not matching the value '%s' in .", ecValue, attName, scValue)); } } private void checkECCanReorderAgainstSC (String attName, CanReorder scValue, String ecValue) { if ( scValue == CanReorder.FIRSTNO ) { if ( ecValue.equals(Const.VALUE_YES) ) { error(String.format("The value '%s' for '%s' in is not matching the value '%s' in .", ecValue, attName, scValue)); } } else if ( !scValue.toString().equals(ecValue) ) { error(String.format("The value '%s' for '%s' in is not matching the value '%s' in .", ecValue, attName, scValue)); } } private void checkECValueAgainstSC (String attName, boolean scValue, boolean ecValue) { if ( scValue != ecValue ) { error(String.format("The value '%s' for '%s' in is not matching the value '%s' in .", (ecValue ? "yes" : "no"), attName, (scValue ? "yes" : "no"))); } } private Tag setOtherInlineAttributes (char inlineType, CTag ctag, MTag mtag, boolean paired, boolean isTarget, boolean closingIsolated, CTag opening, Stack dirCtx) { boolean openingIsolated = false; // Just a local flag boolean expliciteTranslate = false; Tag tag = null; if ( ctag != null ) tag = ctag; else tag = mtag; // Assumes mtag is not null boolean closingNonIsolated = (( inlineType == Fragment.CODE_CLOSING ) && ( opening!=null )); // Get the namespaces tag.setExtAttributes(gatherNamespaces(tag.getExtAttributes())); boolean typeChk, canCopyChk, canDeleteChk, canOverlapChk, subTypeChk, copyOfChk, dirChk, canReorderChk; typeChk = canCopyChk = canDeleteChk = canOverlapChk = subTypeChk = copyOfChk = dirChk = canReorderChk = false; // Get the type in case we must switch the marker class String value = reader.getAttributeValue("", Const.ATTR_TYPE); if ( value != null ) { switch ( inlineType ) { case Fragment.CODE_OPENING: case Fragment.CODE_CLOSING: case Fragment.CODE_STANDALONE: if ( closingNonIsolated ) { checkECValueAgainstSC(Const.ATTR_TYPE, opening.getType(), value); typeChk = true; } else ctag.setType(value); break; case Fragment.MARKER_OPENING: // Switch the marker if ( value.equals(TermTag.TYPE_TERM) || value.equals(TermTag.TYPE_ITSTERMNO) ) { // Switch the marker to a TermTag mtag = new TermTag(mtag, value, inheritedData.peek().getAnnotatorsRef()); tag = mtag; break; } // Else: fall thru (normal annotation) mtag.setType(value); break; case Fragment.MARKER_CLOSING: // Nothing to set (the value will always be set from break; } } // Get the attributes for ( int i=0; i is not matching the value '%s' in .", dir.toString(), dirCtx.peek().toString())); } dirChk = true; } else { ctag.setDir(dir); } // Update context //TODO: push/pop for isolated tags may cause problem here if (inlineType == Fragment.CODE_OPENING) dirCtx.push(dir); else { if (!dirCtx.isEmpty()) dirCtx.pop(); } break; default: error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_VALUE: switch (inlineType) { case Fragment.MARKER_OPENING: case Fragment.MARKER_CLOSING: mtag.setValue(value); break; default: error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_REF: switch (inlineType) { case Fragment.MARKER_OPENING: case Fragment.MARKER_CLOSING: mtag.setRef(value); break; default: error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_TRANSLATE: switch (inlineType) { case Fragment.MARKER_OPENING: case Fragment.MARKER_CLOSING: canBeYesOrNo(Const.ATTR_TRANSLATE, value); mtag.setTranslate(Const.VALUE_YES.equals(value)); expliciteTranslate = true; break; default: error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_ISOLATED: switch (inlineType) { case Fragment.CODE_OPENING: // Just check the value. It is not stored in the code // Because isolation is determine at output time canBeYesOrNo(Const.ATTR_ISOLATED, value); openingIsolated = true; break; case Fragment.CODE_CLOSING: // Handled in the caller method // And passed as a parameter (but let's set it anyway) closingIsolated = true; break; default: error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_CANOVERLAP: switch (inlineType) { case Fragment.CODE_OPENING: case Fragment.CODE_CLOSING: boolean yon = getYesOrNo(Const.ATTR_CANOVERLAP, value, !paired); if (closingNonIsolated) { checkECValueAgainstSC(Const.ATTR_CANOVERLAP, opening.getCanOverlap(), yon); canOverlapChk = true; } else ctag.setCanOverlap(yon); break; default: error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_EQUIV: if (!paired) { switch (inlineType) { case Fragment.CODE_OPENING: case Fragment.CODE_CLOSING: case Fragment.CODE_STANDALONE: ctag.setEquiv(value); continue; } } error(String.format("Invalid attribute '%s'.", locName)); break; case Const.ATTR_EQUIVSTART: if (paired) { switch (inlineType) { case Fragment.CODE_OPENING: ctag.setEquiv(value); break; case Fragment.CODE_CLOSING: // Valid but don't set break; } } else { error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_EQUIVEND: if (paired) { switch (inlineType) { case Fragment.CODE_CLOSING: ctag.setEquiv(value); break; case Fragment.CODE_OPENING: // Valid but don't set break; } } else { error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_DISP: if (!paired) { switch (inlineType) { case Fragment.CODE_OPENING: case Fragment.CODE_CLOSING: case Fragment.CODE_STANDALONE: ctag.setDisp(value); continue; } } error(String.format("Invalid attribute '%s'.", locName)); break; case Const.ATTR_DISPSTART: if (paired) { switch (inlineType) { case Fragment.CODE_OPENING: ctag.setDisp(value); break; case Fragment.CODE_CLOSING: // Valid but don't set break; } } else { error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_DISPEND: if (paired) { switch (inlineType) { case Fragment.CODE_CLOSING: ctag.setDisp(value); break; case Fragment.CODE_OPENING: // Valid but don't set break; } } else { error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_SUBFLOWS: if (!paired) { switch (inlineType) { case Fragment.CODE_OPENING: case Fragment.CODE_CLOSING: case Fragment.CODE_STANDALONE: ctag.setSubFlows(value); for (String id : ctag.getSubFlowsIds()) { subFlowIds.add(id); } break; } } else { error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_SUBFLOWSSTART: if (paired) { switch (inlineType) { case Fragment.CODE_OPENING: ctag.setSubFlows(value); for (String id : ctag.getSubFlowsIds()) { subFlowIds.add(id); } break; case Fragment.CODE_CLOSING: // Valid but don't set break; } } else { error(String.format("Invalid attribute '%s'.", locName)); } break; case Const.ATTR_SUBFLOWSEND: if (paired) { switch (inlineType) { case Fragment.CODE_CLOSING: ctag.setSubFlows(value); for (String id : ctag.getSubFlowsIds()) { subFlowIds.add(id); } break; case Fragment.CODE_OPENING: // Valid but don't set break; } } else { error(String.format("Invalid attribute '%s'.", locName)); } break; default: // Invalid attribute in core namespace error(String.format("Invalid attribute '%s'.", locName)); break; } } else { // Other namespaces than the core QName qname = reader.getAttributeName(i); if ( ctag != null ) { // For inline codes, this is restricted to modules only if ( !qname.getNamespaceURI().startsWith(Const.NS_XLIFF_MODSTART) ) { // This will be triggered on the opening element in the case of paired markers error(String.format( "Invalid extension attribute (%s). " + "Only attributes from core and modules can be used in inline codes.", qname.toString())); } if ( inlineType == Fragment.CODE_CLOSING ) { // Do not copy modules attributes in closing case of a paired code if ( !paired ) { // Module extension are OK, but only not in non-isolated closing markers if ( !closingIsolated ) { error(String.format( "Invalid module attribute %s. " + "It is allowed in only when the element is isolated.", qname.toString())); } // Else: OK addExtAttribute(ctag, i, false); } } else { // Opening and placeholder cases addExtAttribute(ctag, i, false); } } else { // Any extension attribute is allowed for the opening annotations if ( inlineType == Fragment.MARKER_CLOSING ) { if ( !paired ) { error(String.format( "Invalid attribute %s. " + "No module or extension attribute is allowed in .", qname.toString())); } // Else: Do nothing we are processing // Extensions/modules are not copied } else { // Opening annotation // Process ITS attributes if ( tag instanceof TermTag ) { itsReader.readTerminology((TermTag)mtag, inheritedData.peek().getAnnotatorsRef()); } else { // Other ITS annotation itsReader.readAttributes(unit, mtag, inheritedData.peek().getAnnotatorsRef()); } // Process any other addExtAttribute(mtag, i, true); // Assumes am is not null } } } } // End of for all attributes // Code validations if ( ctag != null ) { // If we do not have canReorder='yes' // then canRemove and canCopy must be 'no' if ( ctag.getCanReorder() != CanReorder.YES ) { if ( ctag.getCanCopy() || ctag.getCanDelete() ) { error("If canReorder is not set to 'yes' then canCopy and canDelete must be set to 'no'."); } } // Check type/subType values try { ctag.verifyTypeSubTypeValues(); } catch ( InvalidParameterException e ) { error(e.getLocalizedMessage()); } // Store isolated information // this will be used when finishing to process the unit for validation if ( closingIsolated || openingIsolated ) { if ( isTarget ) trgIsolated.add(ctag); else srcIsolated.add(ctag); } // Check default values of ec if ( closingNonIsolated ) { // For any attributes not specified: checks the default against the opening if ( !canCopyChk ) { if ( !ctag.getCanCopy() ) { error(String.format( "In with startRef='%s', the default canCopy value does not match the one of the corresponding ('%s').", ctag.getId(), (opening.getCanCopy() ? "yes" : "no"))); } } if ( !canDeleteChk ) { if ( !ctag.getCanDelete() ) { error(String.format( "In with startRef='%s', the default canDelete value does not match the one of the corresponding ('%s').", ctag.getId(), (opening.getCanDelete() ? "yes" : "no"))); } } if ( !typeChk ) { if ( ctag.getType() != null ) { error(String.format( "In with startRef='%s', the default type value does not match the one of the corresponding ('%s').", ctag.getId(), opening.getType())); } } if ( !subTypeChk ) { if ( ctag.getSubType() != null ) { error(String.format( "In with startRef='%s', the default subType value does not match the one of the corresponding ('%s').", ctag.getId(), opening.getSubType())); } } if ( !copyOfChk ) { if ( ctag.getCopyOf() != null ) { error(String.format( "In with startRef='%s', the default copyOf value does not match the one of the corresponding ('%s').", ctag.getId(), opening.getCopyOf())); } } if ( !canReorderChk ) { if ( ctag.getCanReorder() != CanReorder.YES ) { error(String.format( "In with startRef='%s', the default canReorder value is not valid for the corresponding value ('%s').", ctag.getId(), opening.getCanReorder())); } } // if ( !dirChk ) { // if ( ctag.getDir() != dirCtx.peek() ) { // error(String.format( // "In with startRef='%s', the default dir value does not match the one of the corresponding ('%s').", // ctag.getId(), opening.getDir())); // } // } } } else { // Annotation validation if ( mtag.getTagType() == TagType.OPENING ) { checkAnnotation(mtag, expliciteTranslate); } } return tag; } private String setDataRef (CTag code) { // Try to see if there are outside data defined String tmp = reader.getAttributeValue("", Const.ATTR_DATAREF); if ( cannotBeEmpty(Const.ATTR_DATAREF, tmp) ) { // The actual original data may not be available yet. // We just set the id to use. // the copy is done when we finish to parse the block code.setDataRef(tmp); return tmp; } return null; } private ExtAttributes gatherExtAttributes (boolean isExtElement) { ExtAttributes attrs = null; // Get the namespaces for ( int i=0; i children = elem.getChildren(); while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: // Else: this is a child element: read it recursively pushXMLAttributes(); children.add(readExtElement()); break; case XMLStreamReader.END_ELEMENT: if ( reader.getName().equals(elem.getQName()) ) { // Done popXMLAttributes(); return elem; } break; case XMLStreamReader.CHARACTERS: case XMLStreamReader.SPACE: children.add(new ExtContent(reader.getText())); break; case XMLStreamReader.CDATA: children.add(new ExtContent(reader.getText(), true)); break; case XMLStreamReader.PROCESSING_INSTRUCTION: children.add(new ProcessingInstruction("")); break; } } throw new IOException("Unexpected end of file."); } catch ( XMLStreamException | IOException e ) { error("Error adding an extension element. "+e.getMessage()); } return null; } private void processChangeTracking (IWithChangeTrack parent) throws XMLStreamException { if ( !reader.getLocalName().equals(ChangeTrack.TAG_NAME) ) { error(String.format("Invalid element '%s'", reader.getName().toString())); } if ( parent.hasChangeTrack() ) { error(String.format("Too many elements '%s'", reader.getName().toString())); } ChangeTrack changeTrack = new ChangeTrack(); while (reader.hasNext()) { switch (reader.next()) { case XMLStreamReader.START_ELEMENT: processRevisions(changeTrack); break; case XMLStreamReader.END_ELEMENT: popXMLAttributes(); if ( changeTrack.isEmpty() ) { error("You must have at least one <" + Revisions.TAG_NAME + "> element in a <" + ChangeTrack.TAG_NAME + "> element."); } parent.setChangeTrack(changeTrack); return; default: break; } } } private void processRevisions (ChangeTrack changeTrack) throws XMLStreamException { String tmp = reader.getLocalName(); String nsUri = reader.getNamespaceURI(); if ( !tmp.equals(Revisions.TAG_NAME) || !nsUri.equals(Const.NS_XLIFF_TRACKING20) ) { error(String.format("Invalid element '%s' in <" + ChangeTrack.TAG_NAME + ">.", reader.getName().toString())); } // Match match = new Match(); Revisions revisions = new Revisions(); pushXMLAttributes(); // Get the namespaces revisions.setExtAttributes(gatherNamespaces(revisions.getExtAttributes())); // Process the attributes for (int i = 0; i < reader.getAttributeCount(); i++) { String locName = reader.getAttributeLocalName(i); String value = reader.getAttributeValue(i); String ns = reader.getAttributeNamespace(i); // Element is in the ctr namespace so attribute not prefixed are ctr if ( Util.isNoE(ns) ) { switch (locName) { case Revisions.APPLIES_TO_ATTR_NAME: revisions.setAppliesTo(value); break; case Revisions.REF_ATTR_NAME: revisions.setRef(value); break; case Revisions.CURRENT_VERSION_ATTR_NAME: revisions.setCurrentVersion(value); break; default: // Invalid attribute in ctr namespace error(String.format("Invalid attribute '%s'.", locName)); break; } } else { // Other namespaces than ctr -> extension attributes addExtAttribute(revisions, i, false); } } if ( revisions.getAppliesTo() == null ) { error("Attribute " + Revisions.APPLIES_TO_ATTR_NAME + " is required for <" + Revisions.TAG_NAME + "> element."); } // Read the elements while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: processRevision(revisions); break; case XMLStreamReader.END_ELEMENT: popXMLAttributes(); if ( revisions.isEmpty() ) { error("You must have at least one <" + Revision.TAG_NAME + "> element in a <" + Revisions.TAG_NAME + "> element."); } // Check current version if ( revisions.getCurrentVersion() != null && !revisions.getCurrentVersion().isEmpty() ) { boolean found = false; for ( Revision rev : revisions ) { String ver = rev.getVersion(); if (( ver != null ) && ver.equals(revisions.getCurrentVersion()) ) { found = true; break; } } if ( !found ) { error(String.format("The '%s' '%s' was not found in the list of '%s' elements.", Revisions.CURRENT_VERSION_ATTR_NAME, revisions.getCurrentVersion(), Revision.TAG_NAME)); } } changeTrack.add(revisions); return; default: break; } } } private void processRevision (Revisions revisions) throws XMLStreamException { String tmp = reader.getLocalName(); String nsUri = reader.getNamespaceURI(); if ( !tmp.equals(Revision.TAG_NAME) || !nsUri.equals(Const.NS_XLIFF_TRACKING20) ) { error(String.format("Invalid element '%s' in <" + Revisions.TAG_NAME + ">.", reader.getName().toString())); } Revision revision = new Revision(); pushXMLAttributes(); // Get the namespaces revision.setExtAttributes(gatherNamespaces(revision.getExtAttributes())); // Process the attributes for (int i = 0; i < reader.getAttributeCount(); i++) { String locName = reader.getAttributeLocalName(i); String value = reader.getAttributeValue(i); String ns = reader.getAttributeNamespace(i); // Element is in the ctr namespace so attribute not prefixed are ctr if ( Util.isNoE(ns) ) { switch (locName) { case Revision.AUTHOR_ATTR_NAME: revision.setAuthor(value); break; case Revision.DATETIME_ATTR_NAME: revision.setDatetime(value); break; case Revision.VERSION_ATTR_NAME: revision.setVersion(value); break; default: // Invalid attribute in ctr namespace error(String.format("Invalid attribute '%s'.", locName)); break; } } else { // Other namespaces than ctr -> extension attributes addExtAttribute(revision, i, false); } } // Read the elements while (reader.hasNext()) { switch (reader.next()) { case XMLStreamReader.START_ELEMENT: processItem(revision); break; case XMLStreamReader.END_ELEMENT: popXMLAttributes(); if ( revision.isEmpty() ) { error(String.format("You must have at least one '%s' element in a '%s' element.", Item.TAG_NAME, Revision.TAG_NAME)); } revisions.add(revision); return; default: break; } } } private void processItem (Revision revision) throws XMLStreamException { String tmp = reader.getLocalName(); String nsUri = reader.getNamespaceURI(); if ( !tmp.equals(Item.TAG_NAME) || !nsUri.equals(Const.NS_XLIFF_TRACKING20) ) { error(String.format("Invalid element '%s' in '%s'.", reader.getName().toString(), Revision.TAG_NAME)); } Item item = new Item(); pushXMLAttributes(); // Get the namespaces item.setExtAttributes(gatherNamespaces(item.getExtAttributes())); // Process the attributes for (int i=0; i < reader.getAttributeCount(); i++) { String locName = reader.getAttributeLocalName(i); String value = reader.getAttributeValue(i); String ns = reader.getAttributeNamespace(i); // Element is in the ctr namespace so attribute not prefixed are ctr if ( Util.isNoE(ns) ) { if ( locName.equals(Item.PROPERTY_ATTR_NAME) ) { item.setProperty(value); } else { // Invalid attribute in ctr namespace error(String.format("Invalid attribute '%s'.", locName)); } } else { // Other namespaces than ctr -> extension attributes addExtAttribute(item, i, false); } } StringBuilder sb = new StringBuilder(); while ( reader.hasNext() ) { switch (reader.next()) { case XMLStreamReader.CHARACTERS: sb.append(reader.getText()); break; case XMLStreamReader.START_ELEMENT: error(String.format("The '%s' element has only text content.", Item.TAG_NAME)); case XMLStreamReader.END_ELEMENT: // Can only be the end of the popXMLAttributes(); item.setText(sb.toString()); revision.add(item); return; } } } protected void processMatches () throws XMLStreamException { if ( !reader.getLocalName().equals("matches") ) { error(String.format("Invalid element '%s'", reader.getName().toString())); } pushSpecialIds(); Matches matches = new Matches(); while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: processMatch(matches); break; case XMLStreamReader.END_ELEMENT: // can be only the end of matches popXMLAttributes(); if ( matches.isEmpty() ) { error("You must have at least one element in a element."); } unit.setMatches(matches); popSpecialIds(); // Clear the list of isolated element because the one found in matches may conflict // with the ones in the segments 9and we check only the ones in segments) srcIsolated.clear(); trgIsolated.clear(); return; } } } private void processMatch (Matches matches) throws XMLStreamException { String tmp = reader.getLocalName(); String nsUri = reader.getNamespaceURI(); if ( !tmp.equals("match") || !nsUri.equals(Const.NS_XLIFF_MATCHES20) ) { error(String.format("Invalid element '%s' in .", reader.getName().toString())); } Match match = new Match(); Map matchODM = null; ExtElements extElems = null; pushXMLAttributes(); // Get the namespaces match.setExtAttributes(gatherNamespaces(match.getExtAttributes())); // Get the current annotatorsRef data AnnotatorsRef ar = inheritedData.peek().getAnnotatorsRef(); if ( ar != null ) match.setAnnoatorRef(ar.get(DataCategories.MTCONFIDENCE)); // Process the attributes for ( int i=0; i has a subType, type must be set explicitly."); } match.setSubType(value); } break; case "id": mustBeValidNmtoken(Const.ATTR_ID, value, true); checkAndAddSpecialId(Const.NS_XLIFF_MATCHES20, value); match.setId(value); break; case "similarity": match.setSimilarity(Double.valueOf(value)); break; case "matchQuality": match.setMatchQuality(Double.valueOf(value)); break; case "matchSuitability": match.setMatchSuitability(Double.valueOf(value)); break; case "origin": match.setOrigin(value); break; case "reference": if (canBeYesOrNo("reference", value)) { match.setReference(value.equals(Const.VALUE_YES)); } break; default: // Invalid attribute in mtc namespace error(String.format("Invalid attribute '%s'.", locName)); break; } } else if ( ns.equals(Const.NS_XML) ) { if ( locName.equals("lang") ) { error("The attribute xml:lang is not allowed in ."); } } else if ( ns.equals(Const.NS_ITS) ) { if ( locName.equals(ITSReader.ANNOTATORSREF) ) { AnnotatorsRef tmpAR = itsReader.readAnnotatorsRef(false, ar); if ( tmpAR != null ) { match.setAnnoatorRef(tmpAR.get(DataCategories.MTCONFIDENCE)); } } else { warning("Unexpected ITS attribute: "+locName); } } else { // Other namespaces than mtc -> extension attributes addExtAttribute(match, i, false); } } // Temporary part to read the source and target fragments Part part = new Part(match.getStore()); int state = 0; // optional meta, then optional originalData, then source then target, then extensions // Read the elements while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); pushXMLAttributes(); if ( nsUri.equals(Const.NS_XLIFF_METADATA20) ) { if ( state > 0 ) error(String.format("Element '%s' misplaced.", reader.getName().toString())); processMetadata(match); state = 1; } else if ( nsUri.equals(Const.NS_XLIFF_CORE20) ) { switch ( tmp ) { case Const.ELEM_ORIGINALDATA: // OK when state < 2 if ( state > 1 ) error(String.format("Element '%s' misplaced.", reader.getName().toString())); matchODM = processOriginalData(); state = 2; break; case Const.ELEM_SOURCE: // OK when state < 3 if ( state > 2 ) error(String.format("Element '%s' misplaced.", reader.getName().toString())); processContent(part, false, false); match.setSource(part.getSource()); state = 3; break; case Const.ELEM_TARGET: // OK when state < 4 if ( state > 3 ) error(String.format("Element '%s' misplaced.", reader.getName().toString())); processContent(part, true, match.isReference()); match.setTarget(part.getTarget()); state = 4; break; default: error(String.format("Invalid element '%s'", reader.getName().toString())); } } else { // OK when state > 3 if ( state < 4 ) error(String.format("Element '%s' misplaced.", reader.getName().toString())); if ( locValidator != null ) locValidator.reset(); extElems = processExtElement("mtc:match", extElems); } break; case XMLStreamReader.END_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); if ( nsUri.equals(Const.NS_XLIFF_MATCHES20) ) { // Can be only the end of match if ( state < 4 ) error("Missing element(s) in ."); copyOriginalDataToCodes(match.getStore(), matchODM); matches.add(match); if ( extElems != null ) match.setExtElements(extElems); popXMLAttributes(); return; } break; } } } private void processGlossary () throws XMLStreamException { if ( !reader.getLocalName().equals("glossary") ) { error(String.format("Invalid element '%s'", reader.getName().toString())); } pushSpecialIds(); Glossary glossary = new Glossary(); while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: processGlossEntry(glossary); break; case XMLStreamReader.END_ELEMENT: // can be only the end of matches popXMLAttributes(); if ( glossary.isEmpty() ) { error("You must have at least one element in a element."); } unit.setGlossary(glossary); popSpecialIds(); return; } } } private void processGlossEntry (Glossary glossary) throws XMLStreamException { String tmp = reader.getLocalName(); String nsUri = reader.getNamespaceURI(); if ( !tmp.equals("glossEntry") || !nsUri.equals(Const.NS_XLIFF_GLOSSARY20) ) { error(String.format("Invalid element '%s' in .", reader.getName().toString())); } GlossEntry entry = new GlossEntry(); ExtElements extElems = null; pushXMLAttributes(); // Get the namespaces entry.setExtAttributes(gatherNamespaces(entry.getExtAttributes())); // Process the attributes for ( int i=0; i extension attributes addExtAttribute(entry, i, false); } } int state = 0; // term, optional translation(s), optional definition, then extensions // Read the elements while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); pushXMLAttributes(); if ( nsUri.equals(Const.NS_XLIFF_GLOSSARY20) ) { switch ( tmp ) { case "term": // OK when state == 0 if ( state != 0 ) error(String.format("Element '%s' misplaced.", reader.getName().toString())); // Get the attributes for ( int i=0; i extension attributes addExtAttribute(entry.getTerm(), i, false); } } // Get the content entry.getTerm().setText(readTextContent(true)); state = 1; break; case "translation": // OK when state == 1 if ( state != 1 ) error(String.format("Element '%s' misplaced.", reader.getName().toString())); Translation trans = new Translation((String)null); // Get the attributes for ( int i=0; i extension attributes addExtAttribute(trans, i, false); } } // Get the content trans.setText(readTextContent(true)); entry.getTranslations().add(trans); // State will change when we see an element different from translation break; case "definition": // OK when state == 1 or 2 if (( state < 1 ) || ( state > 2 )) error(String.format("Element '%s' misplaced.", reader.getName().toString())); entry.setDefinition(new Definition("")); // Default // Get the attributes for ( int i=0; i extension attributes addExtAttribute(entry.getDefinition(), i, false); } } // Get the content entry.getDefinition().setText(readTextContent(true)); state = 3; break; default: error(String.format("Invalid element '%s'", reader.getName().toString())); } } else { // OK when state == 1, 2 or 3 if ( state < 1 ) error(String.format("Element '%s' misplaced.", reader.getName().toString())); if ( locValidator != null ) locValidator.reset(); extElems = processExtElement("gls:glossEntry", extElems); } break; case XMLStreamReader.END_ELEMENT: tmp = reader.getLocalName(); nsUri = reader.getNamespaceURI(); if ( nsUri.equals(Const.NS_XLIFF_GLOSSARY20) ) { // Can be only the end of match if ( state < 1 ) error("Missing element(s) in ."); if ((( entry.getDefinition() == null ) || Util.isNoE(entry.getDefinition().getText()) ) && entry.getTranslations().isEmpty() ) { error("A must have at least a or a ."); } glossary.add(entry); if ( extElems != null ) entry.setExtElements(extElems); popXMLAttributes(); return; } break; } } } private void processMetadata (IWithMetadata parent) throws XMLStreamException { if ( !reader.getLocalName().equals("metadata") ) { error(String.format("Invalid element '%s'", reader.getName().toString())); } if ( parent.hasMetadata() ) { error(String.format("Too many elements '%s'", reader.getName().toString())); } pushSpecialIds(); Metadata metadata = new Metadata(); for ( int i=0; i invalid error(String.format("Invalid attribute '%s'.", locName)); } } while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: if ( !Const.NS_XLIFF_METADATA20.equals(reader.getNamespaceURI()) || !"metaGroup".equals(reader.getLocalName()) ) { error(String.format("Invalid element '%s' in .", reader.getName().toString())); } processMetaGroup(metadata); break; case XMLStreamReader.END_ELEMENT: // can be only the end of matches popXMLAttributes(); if ( metadata.isEmpty() ) { error("You must have at least one entry a element."); } parent.setMetadata(metadata); popSpecialIds(); return; } } } private void processMetaGroup (IWithMetaGroup parent) throws XMLStreamException { MetaGroup group = new MetaGroup(); // Process the attributes for ( int i=0; i invalid error(String.format("Invalid attribute '%s'.", locName)); } } // Process the content of the group while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: if ( Const.NS_XLIFF_METADATA20.equals(reader.getNamespaceURI()) ) { switch ( reader.getLocalName() ) { case "metaGroup": processMetaGroup(group); // Recursive call continue; case "meta": // Attributes String type = null; for ( int i=0; i invalid error(String.format("Invalid attribute '%s'.", locName)); } } if ( type == null ) { error("The element must have a type attribute."); } group.add(new Meta(type, readTextContent(false))); continue; } // Else: falls thru to error } // Else: error error(String.format("Invalid element '%s' in .", reader.getName().toString())); break; case XMLStreamReader.END_ELEMENT: if ( group.isEmpty() ) { error("You must have at least one entry a element."); } parent.addGroup(group); return; } } } private void processValidation (IWithValidation parent) throws XMLStreamException { if ( !reader.getLocalName().equals("validation") ) { error(String.format("Invalid element '%s'", reader.getName().toString())); } if ( parent.hasValidation() && parent.getValidation().getDeclarationCount()>0 ) { error(String.format("Too many elements '%s'", reader.getName().toString())); } //pushSpecialIds(); // Check if the parent has rules already, if so: use those // otherwise start with the ones in context Validation validation; if ( parent.hasValidation() ) validation = parent.getValidation(); else validation = valContext.peek(); validation.addDeclaration(); // Read the extension attributes validation.setExtAttributes(gatherExtAttributes(false)); while ( reader.hasNext() ) { switch ( reader.next() ) { case XMLStreamReader.START_ELEMENT: if ( !Const.NS_XLIFF_VALIDATION20.equals(reader.getNamespaceURI()) || !"rule".equals(reader.getLocalName()) ) { error(String.format("Invalid element '%s' in .", reader.getName().toString())); } processValidationRule(validation); break; case XMLStreamReader.END_ELEMENT: // can be only the end of validation popXMLAttributes(); if ( validation.isEmpty() ) { error("You must have at least one entry a element."); } parent.setValidation(validation); //popSpecialIds(); return; } } } private void processValidationRule (Validation validation) throws XMLStreamException { Rule rule = new Rule("isPresent", null); String type = null; String occurs = null; String existsInSource = null; String caseSensitive = null; String disabled = null; String normalization = null; // Process the attributes for ( int i=0; i