/* Copyright (c) 2004-2008, Dennis M. Sosnoski All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of JiBX nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.jibx.binding.model; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.jibx.binding.classes.ClassCache; import org.jibx.binding.util.StringArray; import org.jibx.runtime.BindingDirectory; import org.jibx.runtime.EnumSet; import org.jibx.runtime.IBindingFactory; import org.jibx.runtime.IMarshallingContext; import org.jibx.runtime.IUnmarshallingContext; import org.jibx.runtime.IXMLWriter; import org.jibx.runtime.JiBXException; import org.jibx.runtime.Utility; import org.jibx.runtime.impl.UnmarshallingContext; /** * Model component for binding element. * * @author Dennis M. Sosnoski */ public class BindingElement extends NestingElementBase { /** Enumeration of allowed attribute names */ public static final StringArray s_allowedAttributes = new StringArray(new String[] { "add-constructors", "direction", "force-classes", "forwards", "name", "package", "track-source" }, NestingElementBase.s_allowedAttributes); // // Value set information public static final int IN_BINDING = 0; public static final int OUT_BINDING = 1; public static final int BOTH_BINDING = 2; /*package*/ static final EnumSet s_directionEnum = new EnumSet(IN_BINDING, new String[] { "input", "output", "both" }); // // Instance data /** Binding name. */ private String m_name; /** Binding direction. */ private String m_direction; /** Input binding flag. */ private boolean m_isInput; /** Output binding flag. */ private boolean m_isOutput; /** Support forward references to IDs flag. */ private boolean m_isForward; /** Generate souce tracking interface flag. */ private boolean m_isTrackSource; /** Generate souce tracking interface flag. */ private boolean m_isForceClasses; /** Add default constructors where needed flag. */ private boolean m_isAddConstructors; /** Package for generated context factory. */ private String m_targetPackage; /** Base URL for use with relative include paths. */ private URL m_baseUrl; /** Set of paths for includes. */ private final Set m_includePaths; /** Map from include path to actual binding. */ private final Map m_includeBindings; /** List of child elements. */ private final ArrayList m_children; /** Set of class names which can be referenced by ID. */ private Set m_idClassSet; /** List of namespace declarations to be added on output (lazy create, null if none). */ private ArrayList m_namespaceDeclares; /** * Default constructor. */ public BindingElement() { super(BINDING_ELEMENT); m_includePaths = new HashSet(); m_includeBindings = new HashMap(); m_children = new ArrayList(); m_isForward = true; } /** * Set binding name. * * @param name binding definition name */ public void setName(String name) { m_name = name; } /** * Get binding name. * * @return binding definition name */ public String getName() { return m_name; } /** * Set forward references to IDs be supported in XML. * * @param forward true if forward references supported, * false if not */ public void setForward(boolean forward) { m_isForward = forward; } /** * Check if forward references to IDs must be supported in XML. * * @return true if forward references required, * false if not */ public boolean isForward() { return m_isForward; } /** * Set source position tracking for unmarshalling. * * @param track true if source position tracking enabled, * false if not */ public void setTrackSource(boolean track) { m_isTrackSource = track; } /** * Check if source position tracking enabled for unmarshalling. * * @return true if source position tracking enabled, * false if not */ public boolean isTrackSource() { return m_isTrackSource; } /** * Set force marshaller/unmarshaller class creation for top-level non-base * abstract mappings. * * @param force true if class generation forced, * false if not */ public void setForceClasses(boolean force) { m_isForceClasses = force; } /** * Check if marshaller/unmarshaller class creation for top-level non-base * abstract mappings is forced. * * @return true if class generation forced, * false if not */ public boolean isForceClasses() { return m_isForceClasses; } /** * Set default constructor generation. * * @param add true if constructors should be added, * false if not */ public void setAddConstructors(boolean add) { m_isAddConstructors = add; } /** * Check if default constructor generation is enabled. * * @return true if default constructor generation enabled, * false if not */ public boolean isAddConstructors() { return m_isAddConstructors; } /** * Set package for generated context factory class. * * @param pack generated context factory package */ public void setTargetPackage(String pack) { m_targetPackage = pack; } /** * Get package for generated context factory class. * * @return package for generated context factory */ public String getTargetPackage() { return m_targetPackage; } /** * Set base URL for relative include paths. * * @param base */ public void setBaseUrl(URL base) { m_baseUrl = base; } /** * Get base URL for relative include paths. * * @return base URL */ public URL getBaseUrl() { return m_baseUrl; } /** * Set the correct direction text. This should be used whenever the * individual in and out flags are set, so that modifications are output * correctly when a binding is marshalled. */ private void setDirection() { int direct = m_isInput ? (m_isOutput ? BOTH_BINDING : IN_BINDING) : OUT_BINDING; m_direction = s_directionEnum.getName(direct); } /** * Set binding component applies for marshalling XML. * * @param out true if binding supports output, * false if not */ public void setOutBinding(boolean out) { m_isOutput = out; setDirection(); } /** * Check if this binding component applies for marshalling XML. * * @return true if binding supports output, false * if not */ public boolean isOutBinding() { return m_isOutput; } /** * Set binding component applies for unmarshalling XML. * * @param in true if binding supports input, * false if not */ public void setInBinding(boolean in) { m_isInput = in; setDirection(); } /** * Check if this binding component applies for unmarshalling XML. * * @return true if binding supports input, false * if not */ public boolean isInBinding() { return m_isInput; } /** * Add include path to set processed. * * @param path * @return true if new path, false if duplicate */ public boolean addIncludePath(String path) { return m_includePaths.add(path); } /** * Get included binding. If the binding was supplied directly it's just * returned; otherwise, it's read from the URL. This method should only be * called if {@link #addIncludePath(String)} returns true, * so that each unique included binding is only processed once. * * @param url binding path * @param root binding containing the include * @param vctx validation context * @return binding * @throws IOException * @throws JiBXException */ public BindingElement getIncludeBinding(URL url, BindingElement root, ValidationContext vctx) throws IOException, JiBXException { String path = url.toExternalForm(); BindingElement bind = (BindingElement)m_includeBindings.get(path); if (bind == null) { // get base name from path path = path.replace('\\', '/'); int split = path.lastIndexOf('/'); String fname = path; if (split >= 0) { fname = fname.substring(split+1); } // read stream to create object model InputStream is = url.openStream(); bind = BindingElement.readBinding(is, fname, root, vctx); } // set binding base, and context from root context bind.setBaseUrl(url); bind.setDefinitions(root.getDefinitions().getIncludeCopy()); m_includeBindings.put(path, bind); return bind; } /** * Get existing included binding. * * @param url binding path * @return binding if it exists, otherwise null */ public BindingElement getExistingIncludeBinding(URL url) { return (BindingElement)m_includeBindings.get(url.toExternalForm()); } /** * Add binding accessible to includes. This allows bindings to be supplied * directly, without needing to be parsed from an input document. * * @param path URL string identifying the binding (virtual path) * @param bind */ public void addIncludeBinding(String path, BindingElement bind) { if (addIncludePath(path)) { m_includeBindings.put(path, bind); } } /** * Add a class defined with a ID value. This is used to track the classes * with ID values for validating ID references in the binding. If the * binding uses global IDs, the actual ID class is added to the table along * with all interfaces implemented by the class and all superclasses, since * instances of the ID class can be referenced in any of those forms. If the * binding does not use global IDs, only the actual ID class is added, since * references must be type-specific. * * @param clas information for class with ID value */ public void addIdClass(IClass clas) { // create the set if not already present if (m_idClassSet == null) { m_idClassSet = new HashSet(); } // add the class if not already present if (m_idClassSet.add(clas.getName())) { // new class, add all interfaces if not previously defined String[] inames = clas.getInterfaces(); for (int i = 0; i < inames.length; i++) { m_idClassSet.add(inames[i]); } while (clas != null && m_idClassSet.add(clas.getName())) { clas = clas.getSuperClass(); } } } /** * Check if a class can be referenced by ID. This just checks if any classes * compatible with the reference type are bound with ID values. * * @param name fully qualified name of class * @return true if class is bound with an ID, * false if not */ public boolean isIdClass(String name) { if (m_idClassSet == null) { return false; } else { return m_idClassSet.contains(name); } } /** * Add top-level child element. * TODO: should be ElementBase argument, but JiBX doesn't allow yet * * @param child element to be added as child of this element */ public void addTopChild(Object child) { m_children.add(child); } /** * Get list of top-level child elements. * * @return list of child elements, or null if none */ public ArrayList topChildren() { return m_children; } /** * Get iterator for top-level child elements. * * @return iterator for child elements */ public Iterator topChildIterator() { return m_children.iterator(); } /** * Add namespace declaration for output when marshalling. * * @param prefix * @param uri */ public void addNamespaceDecl(String prefix, String uri) { if (m_namespaceDeclares == null) { m_namespaceDeclares = new ArrayList(); } m_namespaceDeclares.add(prefix); m_namespaceDeclares.add(uri); } // // Overrides of base class methods. /* (non-Javadoc) * @see org.jibx.binding.model.ElementBase#hasAttribute() */ public boolean hasAttribute() { throw new IllegalStateException ("Internal error: method should never be called"); } /* (non-Javadoc) * @see org.jibx.binding.model.ElementBase#hasContent() */ public boolean hasContent() { throw new IllegalStateException ("Internal error: method should never be called"); } /* (non-Javadoc) * @see org.jibx.binding.model.ElementBase#isOptional() */ public boolean isOptional() { throw new IllegalStateException ("Internal error: method should never be called"); } /** * Get default style value for child components. This call is only * meaningful after validation. * * @return default style value for child components */ public int getDefaultStyle() { int style = super.getDefaultStyle(); if (style < 0) { style = NestingAttributes.s_styleEnum.getValue("element"); } return style; } // // Marshalling/unmarshalling methods /** * Marshalling hook method to add namespace declarations to <binding> * element. * * @param ictx * @throws IOException */ private void preGet(IMarshallingContext ictx) throws IOException { if (m_namespaceDeclares != null) { // set up information for namespace indexes and prefixes IXMLWriter writer = ictx.getXmlWriter(); String[] uris = new String[m_namespaceDeclares.size()/2]; int[] indexes = new int[uris.length]; String[] prefs = new String[uris.length]; int base = writer.getNamespaceCount(); for (int i = 0; i < uris.length; i++) { indexes[i] = base + i; prefs[i] = (String)m_namespaceDeclares.get(i*2); uris[i] = (String)m_namespaceDeclares.get(i*2+1); } // add the namespace declarations to current element writer.pushExtensionNamespaces(uris); writer.openNamespaces(indexes, prefs); for (int i = 0; i < uris.length; i++) { String prefix = prefs[i]; String name = prefix.length() > 0 ? "xmlns:" + prefix : "xmlns"; writer.addAttribute(0, name, uris[i]); } } } // // Validation methods /** * Make sure all attributes are defined. * * @param ictx unmarshalling context * @exception JiBXException on unmarshalling error */ private void preSet(IUnmarshallingContext ictx) throws JiBXException { // validate the attributes defined for this element validateAttributes(ictx, s_allowedAttributes); // get the unmarshal wrapper UnmarshalWrapper wrapper = (UnmarshalWrapper)ictx.getStackObject(ictx.getStackDepth()-1); if (wrapper.getContainingBinding() != null) { // check attributes not allowed on included binding BindingElement root = wrapper.getContainingBinding(); ValidationContext vctx = wrapper.getValidation(); UnmarshallingContext uctx = (UnmarshallingContext)ictx; for (int i = 0; i < uctx.getAttributeCount(); i++) { // check if nonamespace attribute is in the allowed set String name = uctx.getAttributeName(i); if (uctx.getAttributeNamespace(i).length() == 0) { if (s_allowedAttributes.indexOf(name) >= 0) { String value = uctx.getAttributeValue(i); if ("direction".equals(name)) { if (!root.m_direction.equals(value)) { vctx.addError("'direction' value on included binding must match the root binding", this); } } else if ("track-source".equals(name)) { boolean flag = Utility.parseBoolean(value); if (root.m_isTrackSource != flag) { vctx.addError("'track-source' value on included binding must match the root binding", this); } } else if ("value-style".equals(name)) { if (!root.getStyleName().equals(value)) { vctx.addError("'value-style' value on included binding must match the root binding", this); } } else if ("force-classes".equals(name)) { boolean flag = Utility.parseBoolean(value); if (root.m_isForceClasses != flag) { vctx.addError("'force-classes' value on included binding must match the root binding", this); } } else if ("add-constructors".equals(name)) { boolean flag = Utility.parseBoolean(value); if (root.m_isAddConstructors != flag) { vctx.addError("'add-constructors' value on included binding must match the root binding", this); } } else if ("forwards".equals(name)) { boolean flag = Utility.parseBoolean(value); if (root.m_isForward != flag) { vctx.addError("'forwards' value on included binding must match the root binding", this); } } else { vctx.addError("Attribute '" + name + "' not allowed on included binding", this); } } } } } } /** * Prevalidate all attributes of element in isolation. * * @param vctx validation context */ public void prevalidate(ValidationContext vctx) { // set the direction flags int index = -1; if (m_direction != null) { index = s_directionEnum.getValue(m_direction); if (index < 0) { vctx.addError("Value \"" + m_direction + "\" is not a valid choice for direction"); } } else { index = BOTH_BINDING; } m_isInput = index == IN_BINDING || index == BOTH_BINDING; m_isOutput = index == OUT_BINDING || index == BOTH_BINDING; // check for illegal characters in name if (m_name != null) { int length = m_name.length(); for (int i = 0; i < length; i++) { if (!Character.isJavaIdentifierPart(m_name.charAt(i))) { vctx.addError("Binding name '" + m_name + " contains invalid characters (only Java identifier part characters allowed)", this); break; } } } super.prevalidate(vctx); } private static FormatElement buildFormat(String name, String type, boolean use, String sname, String dname, String dflt) { FormatElement format = new FormatElement(); format.setLabel(name); format.setTypeName(type); format.setDefaultFormat(use); format.setSerializerName(sname); format.setDeserializerName(dname); format.setDefaultText(dflt); return format; } private void defineBaseFormat(FormatElement format, DefinitionContext dctx, ValidationContext vctx) { format.prevalidate(vctx); format.validate(vctx); dctx.addFormat(format, vctx); } /** * Run the actual validation of a binding model. * * @param vctx context for controlling validation */ public void runValidation(ValidationContext vctx) { // initially enable both directions for format setup m_isInput = true; m_isOutput = true; // create outer definition context DefinitionContext dctx = new DefinitionContext(null); vctx.setGlobalDefinitions(dctx); for (int i = 0; i < FormatDefaults.s_defaultFormats.length; i++) { defineBaseFormat(FormatDefaults.s_defaultFormats[i], dctx, vctx); } FormatElement format = buildFormat("char.string", "char", false, "org.jibx.runtime.Utility.serializeCharString", "org.jibx.runtime.Utility.deserializeCharString", "0"); format.setDefaultFormat(false); format.prevalidate(vctx); format.validate(vctx); dctx.addFormat(format, vctx); NamespaceElement ns = new NamespaceElement(); ns.setDefaultName("all"); ns.prevalidate(vctx); dctx.addNamespace(ns); // TODO: check for errors in basic configuration // create a definition context for the binding setDefinitions(new DefinitionContext(dctx)); // run the actual validation vctx.prevalidate(this); RegistrationVisitor rvisitor = new RegistrationVisitor(vctx); rvisitor.visitTree(this); vctx.validate(this); } /** * Read a binding definition (possibly as an include) to construct binding * model. * * @param is input stream for reading binding * @param fname name of input file (null if unknown) * @param contain containing binding (null if none) * @param vctx validation context used during unmarshalling * @return root of binding definition model * @throws JiBXException on error in reading binding */ public static BindingElement readBinding(InputStream is, String fname, BindingElement contain, ValidationContext vctx) throws JiBXException { // look up the binding factory IBindingFactory bfact = BindingDirectory.getFactory(BindingElement.class); // unmarshal document to construct objects IUnmarshallingContext uctx = bfact.createUnmarshallingContext(); uctx.setDocument(is, fname, null); uctx.pushObject(new UnmarshalWrapper(vctx, contain)); BindingElement binding = (BindingElement)uctx.unmarshalElement(); uctx.popObject(); return binding; } /** * Read a binding definition to construct binding model. * * @param is input stream for reading binding * @param fname name of input file (null if unknown) * @param vctx validation context used during unmarshalling * @return root of binding definition model * @throws JiBXException on error in reading binding */ public static BindingElement readBinding(InputStream is, String fname, ValidationContext vctx) throws JiBXException { return readBinding(is, fname, null, vctx); } /** * Validate a binding definition. * * @param name binding definition name * @param path binding definition URL * @param is input stream for reading binding * @param vctx validation context to record problems * @return root of binding definition model, or null if error * in unmarshalling * @throws JiBXException on error in binding XML structure */ public static BindingElement validateBinding(String name, URL path, InputStream is, ValidationContext vctx) throws JiBXException { // construct object model for binding BindingElement binding = readBinding(is, name, vctx); binding.setBaseUrl(path); vctx.setBindingRoot(binding); // validate the binding definition binding.runValidation(vctx); // list validation errors ArrayList probs = vctx.getProblems(); if (probs.size() > 0) { for (int i = 0; i < probs.size(); i++) { ValidationProblem prob = (ValidationProblem)probs.get(i); System.out.print(prob.getSeverity() >= ValidationProblem.ERROR_LEVEL ? "Error: " : "Warning: "); System.out.println(prob.getDescription()); } } return binding; } /** * Create a default validation context. * * @return new validation context */ public static ValidationContext newValidationContext() { IClassLocator locate = new IClassLocator() { public IClass getClassInfo(String name) { try { return new ClassWrapper(this, ClassCache.getClassFile(name)); } catch (JiBXException e) { return null; } } }; return new ValidationContext(locate); } /* // test runner // This code only used in testing, to roundtrip binding definitions public static void test(String ipath, String opath, ValidationContext vctx) throws Exception { // validate the binding definition FileInputStream is = new FileInputStream(ipath); URL url = new URL("file://" + ipath); BindingElement binding = validateBinding(ipath, url, is, vctx); // marshal back out for comparison purposes IBindingFactory bfact = BindingDirectory.getFactory(BindingElement.class); IMarshallingContext mctx = bfact.createMarshallingContext(); mctx.setIndent(2); mctx.marshalDocument(binding, null, null, new FileOutputStream(opath)); System.out.println("Wrote output binding " + opath); // compare input document with output document DocumentComparator comp = new DocumentComparator(System.err); boolean match = comp.compare(new FileReader(ipath), new FileReader(opath)); if (!match) { System.err.println("Mismatch from input " + ipath + " to output " + opath); } } // test runner public static void main(String[] args) throws Exception { // configure class loading String[] paths = new String[] { "." }; ClassCache.setPaths(paths); ClassFile.setPaths(paths); ValidationContext vctx = newValidationContext(); // process all bindings listed on command line for (int i = 0; i < args.length; i++) { try { String ipath = args[i]; int split = ipath.lastIndexOf(File.separatorChar); String opath = "x" + ipath.substring(split+1); test(ipath, opath, vctx); } catch (Exception e) { System.err.println("Error handling binding " + args[i]); e.printStackTrace(); } } } */ /** * Inner class as wrapper for binding element on unmarshalling. This * provides a handle for passing the validation context, allowing elements * to check for problems during unmarshalling. */ public static class UnmarshalWrapper { private final ValidationContext m_validationContext; private final BindingElement m_containingBinding; protected UnmarshalWrapper(ValidationContext vctx, BindingElement contain) { m_validationContext = vctx; m_containingBinding = contain; } public ValidationContext getValidation() { return m_validationContext; } public BindingElement getContainingBinding() { return m_containingBinding; } } }