diff --git a/src/helma/objectmodel/dom/XmlConstants.java b/src/helma/objectmodel/dom/XmlConstants.java new file mode 100644 index 00000000..67e79535 --- /dev/null +++ b/src/helma/objectmodel/dom/XmlConstants.java @@ -0,0 +1,8 @@ +package helma.objectmodel.dom; + +public interface XmlConstants { + + public final String NAMESPACE = "http://www.helma.org/"; + public final String DATEFORMAT = "dd.MM.yyyy HH:mm:ss z"; + +} diff --git a/src/helma/objectmodel/dom/XmlConverter.java b/src/helma/objectmodel/dom/XmlConverter.java new file mode 100644 index 00000000..446df157 --- /dev/null +++ b/src/helma/objectmodel/dom/XmlConverter.java @@ -0,0 +1,239 @@ +package helma.objectmodel.dom; + +import java.io.*; +import java.net.*; +import java.util.*; + +import javax.xml.parsers.*; +import org.w3c.dom.*; + +import helma.objectmodel.*; +import helma.util.SystemProperties; + +public class XmlConverter implements XmlConstants { + + private boolean DEBUG=false; + + private Properties props; + + private char defaultSeparator = '_'; + + private int offset = 0; + + public XmlConverter() { + props = new SystemProperties(); + } + + public XmlConverter(String propFile) { + props = new SystemProperties(propFile); + extractProperties(props); + } + + public XmlConverter(File propFile) { + this ( propFile.getAbsolutePath() ); + } + + public XmlConverter(Properties props) { + this.props = props; + extractProperties(props); + } + + public INode convert( String desc ) { + return convert(desc, new TransientNode() ); + } + + public INode convert( String desc, INode helmaNode ) throws RuntimeException { + try { + return convert( new URL(desc), helmaNode ); + } catch ( MalformedURLException notanurl ) { + try { + return convert( new File(desc), helmaNode ); + } catch ( FileNotFoundException notfound ) { + throw new RuntimeException( "couldn't read xml: " + desc ); + } + } catch ( IOException ioerror ) { + throw new RuntimeException( "couldn't read xml: " + desc ); + } + } + + public INode convert( File file, INode helmaNode ) throws RuntimeException, FileNotFoundException { + return convert( new FileInputStream(file), helmaNode ); + } + + public INode convert( URL url, INode helmaNode ) throws RuntimeException, IOException, MalformedURLException { + return convert( url.openConnection().getInputStream(), helmaNode ); + } + + public INode convert( InputStream in, INode helmaNode ) throws RuntimeException { + Document document = XmlUtil.parse (in); + if ( document!=null && document.getDocumentElement()!=null ) { + return convert( document.getDocumentElement(), helmaNode ); + } else { + return helmaNode; + } + } + + public INode convert( Element element, INode helmaNode ) { + offset++; + if (DEBUG) debug("reading " + element.getNodeName() ); + helmaNode.setName( element.getNodeName() ); + String prototype = (String)props.get(element.getNodeName().toLowerCase()+".prototype"); + if ( prototype == null ) + prototype = "HopObject"; + helmaNode.setPrototype( prototype ); + attributes(element, helmaNode); + if ( element.hasChildNodes() ) { + children(element, helmaNode); + } + offset--; + return helmaNode; + } + + /** + * parse xml children and create hopobject-children + */ + private INode children( Element element, helma.objectmodel.INode helmaNode ) { + NodeList list = element.getChildNodes(); + int len = list.getLength(); + StringBuffer textcontent = new StringBuffer(); + String domKey, helmaKey; + for ( int i=0; i append to StringBuffer + if ( childNode.getNodeType()==org.w3c.dom.Node.TEXT_NODE ) { + textcontent.append( childNode.getNodeValue().trim() ); + continue; + } + + // it's some kind of element (property or child) + if ( childNode.getNodeType()==org.w3c.dom.Node.ELEMENT_NODE ) { + + Element childElement = (Element)childNode; + + // get the basic key we have to look for in the properties-table + domKey = (element.getNodeName()+"."+childElement.getNodeName()).toLowerCase(); + + // is there a childtext-2-property mapping? + if ( props!=null && props.containsKey(domKey+"._text") ) { + helmaKey = (String)props.get(domKey+"._text"); + if( helmaKey.equals("") ) + // if property is set but without value, read elementname for this mapping + helmaKey = childElement.getNodeName().replace(':',defaultSeparator); + if (DEBUG) debug("childtext-2-property mapping, helmaKey " + helmaKey + " for domKey " + domKey ); + if ( helmaNode.getString(helmaKey,false)==null ) { + helmaNode.setString( helmaKey, XmlUtil.getTextContent(childNode) ); + if (DEBUG) debug("childtext-2-property mapping, setting " + helmaKey + " as string" ); + } + continue; + } + + // + // is there a simple child-2-property mapping? + // (lets the user define to use only one element and make this a property + // and simply ignore other elements of the same name) + if ( props!=null && props.containsKey(domKey+"._property") ) { + helmaKey = (String)props.get(domKey+"._property"); + // if property is set but without value, read elementname for this mapping: + if ( helmaKey.equals("") ) + helmaKey = childElement.getNodeName().replace(':',defaultSeparator); + if (DEBUG) debug("child-2-property mapping, helmaKey " + helmaKey + " for domKey " + domKey); + if ( helmaNode.getNode(helmaKey,false)==null ) { + convert( childElement, helmaNode.createNode(helmaKey) ); + if (DEBUG) debug( "read " + childElement.toString() + helmaNode.getNode(helmaKey,false).toString() ); + } + continue; + } + + + // + // map it to one of the children-lists + helma.objectmodel.INode newHelmaNode = null; + String childrenMapping = (String)props.get(element.getNodeName().toLowerCase()+"._children"); + // do we need a mapping directly among _children of helmaNode? + // can either be through property elname._children=_all or elname._children=childname + if( childrenMapping!=null && ( childrenMapping.equals("_all") || childrenMapping.equals(childElement.getNodeName()) ) ) { + newHelmaNode = convert(childElement, helmaNode.createNode(null) ); + } + // which name to choose for a virtual subnode: + helmaKey = (String)props.get(domKey); + if ( helmaKey==null ) { + helmaKey = childElement.getNodeName().replace(':',defaultSeparator); + } + // try to get the virtual node + helma.objectmodel.INode worknode = helmaNode.getNode( helmaKey, false ); + if ( worknode==null ) { + // if virtual node doesn't exist, create it + worknode = helmaNode.createNode( helmaKey ); + } + if (DEBUG) debug( "mounting child "+ childElement.getNodeName() + " at worknode " + worknode.toString() ); + // now mount it, possibly re-using the helmaNode that's been created before + if ( newHelmaNode!=null ) { + worknode.addNode(newHelmaNode); + } else { + convert( childElement, worknode.createNode( null ) ); + } + } + // forget about other types (comments etc) + continue; + } + + // if there's some text content for this element, map it: + if ( textcontent.length()>0 ) { + helmaKey = (String)props.get(element.getNodeName().toLowerCase()+"._text"); + if ( helmaKey==null ) + helmaKey = "text"; + helmaNode.setString(helmaKey, textcontent.toString().trim() ); + } + + return helmaNode; + } + + /** + * set element's attributes as properties of helmaNode + */ + private INode attributes( Element element, INode helmaNode ) { + NamedNodeMap nnm = element.getAttributes(); + int len = nnm.getLength(); + for ( int i=0; i"); + writeln (""); + writeln ("" ); + write (""); + write (node,null,0); + writeln (""); + convertedNodes = null; + return true; + } + + /** + * write a hopobject and print all its properties and children. + * if node has already been fully printed, just make a reference here. + */ + public void write (INode node, String name, int level) throws IOException { + if ( ++level>maxLevels ) + return; + prefix.append(indent); + if ( convertedNodes.contains(node) ) { + writeReferenceTag (node, name); + } else { + convertedNodes.addElement (node); + writeTagOpen (node,name); + writeProperties (node,level); + writeChildren (node,level); + writeTagClose (node,name); + } + prefix = prefix.delete( prefix.length()-indent.length(), Integer.MAX_VALUE ); + } + + /** + * loop through properties and print them with their property-name + * as elementname + */ + private void writeProperties (INode node, int level) throws IOException { + Enumeration e = node.properties(); + while ( e.hasMoreElements() ) { + String key = (String)e.nextElement(); + IProperty prop = node.get(key,false); + if ( prop!=null ) { + int type = prop.getType(); + if( type==IProperty.NODE ) { + write (node.getNode(key,false), key, level); + } else { + writeProperty (node.get(key,false)); + } + } + } + } + + public void writeNullProperty (String key) throws IOException { + write (prefix.toString()); + write (indent); + write ("<"); + write (key); + write (" hop:type=\"null\"/>"); + write (LINESEPARATOR); + } + + /** + * write a single property, set attribute type according to type, + * apply xml-encoding. + */ + public void writeProperty (IProperty property) throws IOException { + write (prefix.toString()); + write (indent); + write ("<"); + write (property.getName()); + switch (property.getType()) { + case IProperty.BOOLEAN: + write (" hop:type=\"boolean\""); + break; + case IProperty.FLOAT: + write (" hop:type=\"float\""); + break; + case IProperty.INTEGER: + write (" hop:type=\"integer\""); + break; + } + if ( property.getType()==IProperty.DATE ) { + write (" hop:type=\"date\""); + SimpleDateFormat format = new SimpleDateFormat ( DATEFORMAT ); + write (">"); + write ( format.format (property.getDateValue()) ); + } else { + write (">"); + write ( HtmlEncoder.encodeXml (property.getStringValue()) ); + } + write (""); + write (LINESEPARATOR); + } + + /** + * loop through the children-array and print them as + */ + private void writeChildren (INode node, int level) throws IOException { + Enumeration e = node.getSubnodes(); + while (e.hasMoreElements()) { + INode nextNode = (INode)e.nextElement(); + write (nextNode, "hop:child", level); + } + } + + /** + * write an opening tag for a node. Include id and prototype, use a + * name if parameter is non-empty. + */ + public void writeTagOpen (INode node, String name) throws IOException { + write (prefix.toString()); + write ("<"); + write ( (name==null)?"hopobject" : name); + write (" hop:id=\""); + write (getNodeIdentifier(node)); + write ("\" hop:prototype=\""); + write (getNodePrototype(node)); + write ("\""); + write (">"); + write (LINESEPARATOR); + } + + /** + * write a closing tag for a node + * e.g. + */ + public void writeTagClose (INode node, String name) throws IOException { + write (prefix.toString()); + write (""); + write (LINESEPARATOR); + } + + /** + * write a tag holding a reference to an element that has + * been dumped before. + * e.g. + */ + public void writeReferenceTag (INode node, String name) throws IOException { + write (prefix.toString()); + write ("<"); + write ( (name==null)?"hopobject" : name); + write ( " hop:idref=\""); + write (getNodeIdentifier(node)); + write ("\" hop:prototyperef=\""); + write (getNodePrototype(node)); + write ("\""); + write ("/>"); + write (LINESEPARATOR); + } + + /** + * retrieve prototype-string of a node, defaults to "hopobject" + */ + private String getNodePrototype( INode node ) { + if ( node.getPrototype()==null || "".equals(node.getPrototype()) ) { + return "hopobject"; + } else { + return node.getPrototype(); + } + } + + /** + * TransientNode produces a different ID each time we call the getID()-method + * this is a workaround and uses hashCode if INode stands for a TransientNode. + */ + private String getNodeIdentifier( INode node ) { + try { + TransientNode tmp = (TransientNode)node; + return Integer.toString( tmp.hashCode() ); + } catch ( ClassCastException e ) { + return node.getID(); + } + } + + public void writeln(String str) throws IOException { + write (str); + write (LINESEPARATOR); + } + +} + diff --git a/src/helma/scripting/fesi/extensions/DomExtension.java b/src/helma/scripting/fesi/extensions/DomExtension.java new file mode 100644 index 00000000..374c6c87 --- /dev/null +++ b/src/helma/scripting/fesi/extensions/DomExtension.java @@ -0,0 +1,163 @@ +package helma.scripting.fesi.extensions; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.FileNotFoundException; + +import FESI.Data.*; +import FESI.Exceptions.*; +import FESI.Extensions.*; +import FESI.Interpreter.*; + +import helma.framework.core.Application; +import helma.framework.core.RequestEvaluator; +import helma.objectmodel.INode; +import helma.objectmodel.db.Node; +import helma.objectmodel.dom.*; +import helma.scripting.fesi.ESNode; + +public class DomExtension extends Extension { + + private transient Evaluator evaluator = null; + + public DomExtension() { + super(); + } + + public void initializeExtension(Evaluator evaluator) throws EcmaScriptException { + this.evaluator = evaluator; + GlobalObject go = evaluator.getGlobalObject(); + ObjectPrototype op = (ObjectPrototype) evaluator.getObjectPrototype(); + FunctionPrototype fp = (FunctionPrototype) evaluator.getFunctionPrototype(); + + ESObject globalXml = new GlobalObjectXml("Xml", evaluator, fp); + globalXml.putHiddenProperty ("length",new ESNumber(1)); + globalXml.putHiddenProperty ("load", new XmlLoad ("load", evaluator, fp)); + globalXml.putHiddenProperty ("save", new XmlSave ("save", evaluator, fp)); + globalXml.putHiddenProperty ("create", new XmlCreate ("create", evaluator, fp)); + globalXml.putHiddenProperty ("get", new XmlGet ("get", evaluator, fp)); + go.putHiddenProperty ("Xml", globalXml); + } + + class GlobalObjectXml extends BuiltinFunctionObject { + GlobalObjectXml(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + return doConstruct(thisObject, arguments); + } + public ESObject doConstruct(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + throw new EcmaScriptException("Xml can't be instanced"); + } + } + + class XmlSave extends BuiltinFunctionObject { + XmlSave(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + if ( arguments==null || arguments.length<2 ) + throw new EcmaScriptException("not enough arguments"); + INode node = null; + try { + node = ((ESNode)arguments[1]).getNode(); + } catch ( Exception e ) { + // we definitly need a node + throw new EcmaScriptException("argument is not an hopobject"); + } + try { + File tmpFile = new File(arguments[0].toString()+".tmp."+XmlWriter.generateID()); + XmlWriter writer = new XmlWriter (tmpFile); + boolean result = writer.write(node); + writer.close(); + File finalFile = new File(arguments[0].toString()); + tmpFile.renameTo (finalFile); + this.evaluator.reval.app.logEvent("wrote xml to " + finalFile.getAbsolutePath() ); + } catch (IOException io) { + throw new EcmaScriptException (io.toString()); + } + return ESBoolean.makeBoolean(true); + } + } + + class XmlCreate extends BuiltinFunctionObject { + XmlCreate(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + if ( arguments==null || arguments.length==0 ) + throw new EcmaScriptException("not enough arguments"); + INode node = null; + try { + node = ((ESNode)arguments[0]).getNode(); + } catch ( Exception e ) { + // we definitly need a node + throw new EcmaScriptException("argument is not an hopobject"); + } + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + XmlWriter writer = new XmlWriter (out); + boolean result = writer.write(node); + writer.flush(); + return new ESString (out.toString()); + } catch (IOException io) { + throw new EcmaScriptException (io.toString()); + } + } + } + + class XmlLoad extends BuiltinFunctionObject { + XmlLoad(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + if ( arguments==null || arguments.length==0 ) + throw new EcmaScriptException("no arguments for Xml.load()"); + INode node = null; + try { + node = ((ESNode)arguments[1]).getNode(); + } catch ( Exception e ) { //classcast, arrayindex etc + // make sure we have a node, even if 2nd arg doesn't exist or is not a node + node = new Node ( (String)null, (String)null, this.evaluator.reval.app.getWrappedNodeManager() ); + } + try { + XmlReader reader = new XmlReader (); + INode result = reader.read (arguments[0].toString(),node); + return this.evaluator.reval.getNodeWrapper (result); + } catch ( NoClassDefFoundError e ) { + throw new EcmaScriptException ("Can't load dom-capable xml parser."); + } catch ( RuntimeException f ) { + throw new EcmaScriptException (f.toString()); + } + } + } + + class XmlGet extends BuiltinFunctionObject { + XmlGet(String name, Evaluator evaluator, FunctionPrototype fp) { + super(fp, evaluator, name, 1); + } + public ESValue callFunction(ESObject thisObject, ESValue[] arguments) throws EcmaScriptException { + if ( arguments==null || arguments.length==0 ) + throw new EcmaScriptException("Xml.get() needs a location as an argument"); + try { + XmlConverter converter; + if ( arguments.length>1 ) { + converter = new XmlConverter (arguments[1].toString()); + } else { + converter = new XmlConverter (); + } + INode node = new helma.objectmodel.db.Node ( (String)null, (String)null, this.evaluator.reval.app.getWrappedNodeManager() ); + INode result = converter.convert (arguments[0].toString(),node); + return this.evaluator.reval.getNodeWrapper(result); + } catch ( NoClassDefFoundError e ) { + throw new EcmaScriptException("Can't load dom-capable xml parser."); + } catch ( RuntimeException f ) { + throw new EcmaScriptException(f.toString()); + } + } + } + +} + +