blob: c5d08070e7ecee863fa5e9db527db4766554ea28 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2003, 2004 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.wst.common.internal.emf.resource;
import java.util.List;
import java.util.Stack;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
/**
* The EMF2SAXWriter handles the serialization of EMF Resources using SAX events. SAX events are
* triggered to the content handler as the tree is being parsed. These events can then be written
* into any stream wrapped by the ContentHandler.
*
* @author mdelder
*/
public class EMF2SAXWriter {
public static final String NAMESPACE = "";//"http://java.sun.com/xml/ns/j2ee"; //$NON-NLS-1$
/* Used in those cases where no Attributes are necessary */
private static final Attributes EMPTY_ATTRIBUTES = new AttributesImpl();
/**
* Serialize an EMF resource into an XML Stream using the given ContentHandler. Note that this
* method can also be used to copy a given EMF Resource if the EMF2SAXDocumentHandler is used as
* the given ContentHandler.
*
* @param resource
* @param handler
*/
public void serialize(TranslatorResource resource, ContentHandler handler) throws SAXException {
Translator rootTranslator = resource.getRootTranslator();
EList contents = resource.getContents();
if (contents.size() != 1) {
throw new IllegalStateException("The contents of a resource may only contain one EMF Model Object."); //$NON-NLS-1$
}
handler.startDocument();
EObject element = (EObject) contents.get(0);
serialize(handler, element, rootTranslator, new WriterHints(resource));
handler.endDocument();
}
private void serialize(ContentHandler handler, EObject target, Translator translator, WriterHints hints) throws SAXException {
List mofChildren = null;
Object rawValue = null;
EObject newTarget = null;
Translator currentChildTranslator = null;
Translator nextTranslator = null;
char[] characterData = null;
String convertedValue = null;
Attributes attributes = null;
String childDomName = null;
final int version = hints.getVersion();
/*
* Processing hints are used to remember where are in the iteration of the translator's
* children. see the TranslatorFilter for more information on how this array is used.
*/
int[] processingHints = TranslatorFilter.createProcessingHints();
String targetDomName = translator.getDOMName(target);
attributes = getAttributes(translator, target, hints);
handler.startElement(NAMESPACE, targetDomName, targetDomName, attributes);
currentChildTranslator = TranslatorFilter.getNextObjectTranslator(translator, processingHints[TranslatorFilter.NEXT_START_HINT_INDX], processingHints, target, version);
while (currentChildTranslator != null) {
/* For each Child Translator of the Translator parameter passed into the method */
/* Does the Translator have any MOF Children? */
mofChildren = currentChildTranslator.getMOFChildren(target);
openDomPathIfNecessary(handler, hints, currentChildTranslator, target, mofChildren);
if (currentChildTranslator.isManagedByParent()) {
/*
* Translators which are managed by their parents require less processing -- just
* convert their value to a string and write it out as the content of an XML element
*/
childDomName = currentChildTranslator.getDOMName(target);
if (!currentChildTranslator.isEmptyTag()) {
/* The Translator is not an Empty tag. Its text content is significant */
if (mofChildren.size() > 0) {
for (int j = 0; j < mofChildren.size(); j++) {
/* Text only translators will not have open and close XML elements */
if (!currentChildTranslator.isDOMTextValue())
handler.startElement(NAMESPACE, childDomName, childDomName, EMPTY_ATTRIBUTES);
rawValue = mofChildren.get(j);
/* convertValueToString should always return a non-null String */
convertedValue = currentChildTranslator.convertValueToString(rawValue, target);
characterData = XMLEncoderDecoder.escape(convertedValue).toCharArray();
handler.characters(characterData, 0, characterData.length);
if (!currentChildTranslator.isDOMTextValue())
handler.endElement(NAMESPACE, childDomName, childDomName);
}
}
} else {
/*
* The Translator is an Empty Element (its mere presence has significance) (e.g.
* <cascade-delete/>
*/
if (currentChildTranslator.isBooleanFeature()) {
/* Boolean features may or may not be rendered */
rawValue = mofChildren.get(0);
if (rawValue != null && ((Boolean) rawValue).booleanValue()) {
handler.startElement(NAMESPACE, childDomName, childDomName, EMPTY_ATTRIBUTES);
handler.endElement(NAMESPACE, childDomName, childDomName);
}
} else {
/* Always render any other Empty elements */
handler.startElement(NAMESPACE, childDomName, childDomName, EMPTY_ATTRIBUTES);
handler.endElement(NAMESPACE, childDomName, childDomName);
}
}
} else {
/* The Translator is a more complex feature, handle its processing recursively */
for (int j = 0; j < mofChildren.size(); j++) {
newTarget = (EObject) mofChildren.get(j);
serialize(handler, newTarget, currentChildTranslator, hints);
}
}
/* Fetch the next peer translator */
nextTranslator = TranslatorFilter.getNextObjectTranslator(translator, processingHints[TranslatorFilter.NEXT_START_HINT_INDX], processingHints, target, version);
closeDomPathIfNecessary(handler, hints, currentChildTranslator, nextTranslator, target, mofChildren);
/*
* We needed to invoke closeDomPathIfNecessary() with the peer, now we move on to
* process that peer
*/
currentChildTranslator = nextTranslator;
}
handler.endElement(NAMESPACE, targetDomName, targetDomName);
}
/**
* Determines whether or not a DOM Path should be rendered. This method is particularly useful
* for determining whether Empty XML elements are relevant and should be written to the XML
* stream.
*
* @param target
* The EMF Target of the Translation
* @param currentChildTranslator
* The current Translator
* @param mofChildren
* The mofChildren that were found for the Translator on the Target
* @return
*/
private boolean shouldRenderDomPath(EObject target, Translator currentChildTranslator, List mofChildren) {
return !currentChildTranslator.isEmptyContentSignificant() || (currentChildTranslator.shouldRenderEmptyDOMPath(target) || mofChildren.size() > 0);
}
/**
* openDomPathIfNecessary will write the current DOM Path to the serialization stream if it has
* not been written by a previous peer translator. The processing results in the collapse of
* Peer Translators with matching DOM Paths into a single XML parent element.
*
* @param handler
* The ContentHandler which is writing the XML result
* @param hints
* A Global container for information specific to a single XML document
* @param currentChildTranslator
* The active Translator being processed
* @param target
* The EMF Target of the Translation
* @throws SAXException
*/
private void openDomPathIfNecessary(ContentHandler handler, WriterHints hints, Translator currentChildTranslator, EObject target, List mofChildren) throws SAXException {
/* If the translator does not have a DOM Path, then we do nothing */
if (currentChildTranslator.hasDOMPath() && shouldRenderDomPath(target, currentChildTranslator, mofChildren)) {
String childDomPath = currentChildTranslator.getDOMPath();
/*
* IsDomPathActive() will verify whether this DOM Path has already been written to the
* XML stream
*/
if (!hints.isDomPathActive(childDomPath)) {
/*
* Write an open element for the DOM Path and "remember" that we have written it
*/
handler.startElement(NAMESPACE, childDomPath, childDomPath, EMPTY_ATTRIBUTES);
hints.pushDomPath(childDomPath);
}
}
}
/**
* closeDomPathIfNecessary will determine whether the next peer Translator shares the active DOM
* Path of the current Translator. If the next peer Translator has the same DOM Path, no action
* will be taken (hence condensing the elements into a single XML parent). However, if the DOM
* Path differs (including the Next Peer Translator has no DOM Path) then the current DOM Path
* will be closed (a close XML element is generated.
*
* @param handler
* The ContentHandler which is writing the XML result
* @param hints
* A Global container for information specific to a single XML document
* @param currentChildTranslator
* The last Translator to have completed processing
* @param nextTranslator
* The next peer Translator that will become active
* @param target
* The EMF Target of the Translation
* @throws SAXException
*/
private void closeDomPathIfNecessary(ContentHandler handler, WriterHints hints, Translator currentChildTranslator, Translator nextTranslator, EObject target, List mofChildren) throws SAXException {
if (currentChildTranslator.hasDOMPath() && shouldRenderDomPath(target, currentChildTranslator, mofChildren)) {
String childDomPath = currentChildTranslator.getDOMPath();
if (nextTranslator != null) { /*
* There are more peers after this element, we can peek
* ahead
*/
String nextPeerDomPath = nextTranslator.getDOMPath();
if (nextPeerDomPath == null || !nextPeerDomPath.equals(childDomPath)) {
handler.endElement(NAMESPACE, childDomPath, childDomPath);
hints.popDomPath();
}
} else { /* This was the last child element, we must close the dompath */
handler.endElement(NAMESPACE, childDomPath, childDomPath);
hints.popDomPath();
}
}
}
/**
* Aggregate the Attribute translator children from a given translator. This method will request
* the AttributesImpl object from the WriterHints object. The WriterHints maintains this
* reusable collection to limit the requirement for new object creation.
*
* @param translator
* @param target
* @param hints
* @return an initialized set of Attributes for the given Translator and EMF Target
*/
private Attributes getAttributes(Translator translator, EObject target, WriterHints hints) {
AttributesImpl attributes = hints.getAttributeHolder();
int version = hints.getVersion();
Object rawValue = null;
String convertedValue = null;
String childDomName = null;
Translator attributeTranslator = null;
int[] processingHints = TranslatorFilter.createProcessingHints();
while ((attributeTranslator = TranslatorFilter.getNextAttributeTranslator(translator, processingHints[TranslatorFilter.NEXT_START_HINT_INDX], processingHints, target, version)) != null) {
List mofChildren = attributeTranslator.getMOFChildren(target);
if (mofChildren.size() > 0) {
for (int j = 0; j < mofChildren.size(); j++) {
childDomName = attributeTranslator.getDOMName(target);
rawValue = mofChildren.get(j);
convertedValue = attributeTranslator.convertValueToString(rawValue, target);
convertedValue = XMLEncoderDecoder.escape(convertedValue);
attributes.addAttribute(NAMESPACE, childDomName, childDomName, "String", convertedValue); //$NON-NLS-1$
}
} else {
childDomName = attributeTranslator.getDOMName(target);
convertedValue = (String) attributeTranslator.getMOFValue(target);
if (convertedValue != null)
attributes.addAttribute(NAMESPACE, childDomName, childDomName, "String", convertedValue); //$NON-NLS-1$
}
}
return attributes;
}
/**
* WriterHints is used to "remember" certain pieces of information while the writer is
* processing. Of particular interest are the version and the state of the DOM Path output.
* Consecutive elements with consistent (identical) DOM Paths are collapsed under a single XML
* element.
*
* The WriterHints provides global state between recursive invocations of serialize(). It should
* be not be used to store local data (e.g. data that is only relevant to a single Translator in
* a given context).
*
* The WriterHints also stores an AttributesImpl object that is re-used to store attributes. The
* getAttributes() method will request the Attributes Holder.
*
* @author mdelder
*/
public final class WriterHints {
private final TranslatorResource resource;
private final Stack domStack = new Stack();
private final AttributesImpl attributesImpl = new AttributesImpl();
public WriterHints(TranslatorResource res) {
this.resource = res;
}
/**
* Push a new domPath onto the stack
*
* @param domPath
* a DOMPath which has been written to the XML stream
*/
public void pushDomPath(String domPath) {
if (domPath != null && domPath.length() > 0)
domStack.push(domPath);
}
/**
* Pop the current domPath from the Array
*/
public void popDomPath() {
if (!domStack.isEmpty())
domStack.pop();
}
/**
* Determines if the given DOMPath has already been written to the XML stream
*
* @param domPath
* @return true if the given DOMPath has already been written to the XML stream
*/
public boolean isDomPathActive(String domPath) {
boolean result = false;
if (!domStack.isEmpty()) {
String currentDomPath = (String) domStack.peek();
if (currentDomPath != null && domPath != null)
result = currentDomPath.equals(domPath);
else if (!(currentDomPath == null ^ domPath == null))
result = true;
}
return result;
}
/**
* @return the version of the EMF Resource
*/
public int getVersion() {
return this.resource.getVersionID();
}
/**
* Returns an empty AttributesImpl object to store attributes. Within the context of a given
* WriterHints object (and hence single XML document), the object returned is a singleton.
* The same AttributesImpl object is cleared and reused for each invocation.
*
* @return an empty AttributesImpl object to store attributes
*/
public AttributesImpl getAttributeHolder() {
this.attributesImpl.clear();
return this.attributesImpl;
}
}
}