/*
This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. The license (Mozilla version 1.0) can be read at the MMBase site. See http://www.MMBase.org/license */ package org.mmbase.module.core; import java.util.*; import org.mmbase.module.*; import org.mmbase.module.core.*; import org.mmbase.module.corebuilders.*; import org.mmbase.module.builders.MultiRelations; import org.mmbase.module.gui.html.EditState; import org.mmbase.module.gui.html.MMLanguage; import org.mmbase.util.logging.*; import org.w3c.dom.Document; /** * MMObjectNode is the core of the MMBase system. * This class is what its all about, because the instances of this class hold the content we are using. * All active Nodes with data and relations are MMObjectNodes and make up the * object world that is MMBase (Creating, searching, removing is done by the node's parent, * which is a class extended from MMObjectBuilder) * * @author Daniel Ockeloen * @author Pierre van Rooden * @author Eduard Witteveen * @version $Revision: 1.79 $ $Date: 2002/08/12 21:50:16 $ */ public class MMObjectNode { // activate the new implementations of getRelated() and getRelated(type) private static boolean NEW_RELATED_TEST = true; /** * Logger routine */ private static Logger log = Logging.getLoggerInstance(MMObjectNode.class.getName()); /** * Holds the name - value pairs of this node (the node's fields). * Most nodes will have a 'number' and an 'otype' field, and fields which will differ by builder. * This collection should not be directly queried or changed - * use the SetValue and getXXXValue methods instead. * @scope private */ public Hashtable values=new Hashtable(); /** * Holds the 'extra' name-value pairs (the node's properties) * which are retrieved from the 'properties' table. * @scope private */ public Hashtable properties; /** * Vector whcih stores the key's of the fields that were changed * since the last commit. * @scope private */ public Vector changed=new Vector(); /** * Pointer to the parent builder that is responsible for this node. * Note: this may on occasion (due to optimization) duffer for the node's original builder. * Use {@link #getBuilder()} instead. * @scope private */ public MMObjectBuilder parent; /** * Pointer to the actual builder to which this node belongs. * This value is initialised through the first call to {@link #getBuilder() } */ private MMObjectBuilder builder=null; /** * Used to make fields from multiple nodes (for multilevel for example) * possible. * This is a 'default' value. * XXX: specifying the prefix in the fieldName SHOULD override this field. * @scope private */ public String prefix=""; // Vector with the related nodes to this node private Vector relations=null; // possibly filled with insRels /** * Determines whether this node is virtual. * A virtual node is not persistent (that is, not stored in a table). * @scope private */ protected boolean virtual=false; private static int relation_cache_hits=0; private static int relation_cache_miss=0; /** * Alias name of this node. * XXX: nodes can have multiple aliases. * @scope private */ protected String alias; // object to sync access to properties private Object properties_sync=new Object(); // multirelationsbuilder private MultiRelations multirelations = null; /** * Empty constructor added for javadoc * @deprecated Unused. Should be removed. */ public MMObjectNode() { } /** * Main constructor. * @param parent the node's parent, an instance of the node's builder. */ public MMObjectNode(MMObjectBuilder parent) { if (parent!=null) { this.parent=parent; multirelations = (MultiRelations) this.parent.mmb.getMMObject("multirelations"); } else { log.error("MMObjectNode-> contructor called with parent=null"); throw new NullPointerException("contructor called with parent=null"); } } /** * Returns the actual builder of the node. * Note that it is possible that, due to optimization, a node is currently associated with * another (parent) builder, i.e. a posrel node may be associated with a insrel builder. * This method returns the actual builder. * The node may miss vital information (not retrieved from the database) to act as a node of such * a builder - if you need actual status you need to reload it. * @since MMBase-1.6 * @return the builder of this node */ public MMObjectBuilder getBuilder() { if (builder==null) { int oType=getOType(); if (parent.oType==oType) { builder=parent; } else { builder=parent.mmb.getBuilder(parent.mmb.getTypeDef().getValue(oType)); } } return builder; } /** * Tests whether the data in a node is valid (throws an exception if this is not the case). * @throws org.mmbase.module.core.InvalidDataException * If the data was unrecoverably invalid (the references did not point to existing objects) */ public void testValidData() throws InvalidDataException { parent.testValidData(this); }; /** * Commit the node to the database or other storage system. * This can only be done on a existing (inserted) node. It will use the * changed Vector as its base of what to commit/change. * @return <code>true</code> if the commit was succesfull, <code>false</code> is it failed */ public boolean commit() { return parent.commit (this); } /** * Insert this node into the database or other storage system. * @return the new node key (number field), or -1 if the insert failed */ public int insert(String userName) { return parent.insert(userName,this); } /** * Once an insert is done in the editors, this method is called. * @param ed Contains the current edit state (editor info). The main function of this object is to pass * 'settings' and 'parameters' - value pairs that have been set during the edit process. * @return An <code>int</code> value. It's meaning is undefined. * The basic routine returns -1. * @deprecated This method doesn't seem to fit here, as it references a gui/html object ({@link org.mmbase.module.gui.html.EditState}), * endangering the separation between content and layout, and has an undefined return value. */ public int insertDone(EditState ed) { return parent.insertDone(ed,this); } /** * Check and make last changes before calling {@link #commit} or {@link #insert}. * This method is called by the editor. This differs from {@link MMObjectBuilder#preCommit}, which is called by the database system * <em>during</em> the call to commit or insert. * @param ed Contains the current edit state (editor info). The main function of this object is to pass * 'settings' and 'parameters' - value pairs that have been the during the edit process. * @return An <code>int</code> value. It's meaning is undefined. * The basic routine returns -1. * @deprecated This method doesn't seem to fit here, as it references a gui/html object ({@link org.mmbase.module.gui.html.EditState}), * endangering the separation between content and layout. It also has an undefined return value (as well as a confusing name). */ public int preEdit(EditState ed) { return parent.preEdit(ed,this); } /** * Returns the core of this node in a string. * Used for debugging. * For data exchange use toXML() and getDTD(). * @return the contents of the node as a string. */ public String toString() { String result=""; try { result="prefix='"+prefix+"'"; Enumeration e=values.keys(); while (e.hasMoreElements()) { String key=(String)e.nextElement(); int dbtype=getDBType(key); String value=""+values.get(key); // XXX:should be retrieveValue ? if (result.equals("")) { result=key+"="+dbtype+":'"+value+"'"; } else { result+=","+key+"="+dbtype+":'"+value+"'"; } } } catch(Exception e) {} return result; } /** * Return the node as a string in XML format. * Used for data exchange, though, oddly enough, not by application export. (?) * @return the contents of the node as a xml-formatted string. */ public String toXML() { // call is implemented by its builder so // call the builder with this node if (parent!=null) { return parent.toXML(this); } else { return null; } } /** * Stores a value in the values hashtable. * * @param fieldName the name of the field to change * @param fieldValue the value to assign */ protected void storeValue(String fieldName,Object fieldValue) { values.put(fieldName, fieldValue); } /** * Retrieves a value from the values hashtable. * * @param fieldName the name of the field to change * @return the value of the field */ protected Object retrieveValue(String fieldName) { return values.get(fieldName); } /** * Determines whether the node is virtual. * A virtual node is not persistent (that is, stored in a database table). */ public boolean isVirtual() { return virtual; } /** * Sets a key/value pair in the main values of this node. * Note that if this node is a node in cache, the changes are immediately visible to * everyone, even if the changes are not committed. * The fieldName is added to the (public) 'changed' vector to track changes. * @param fieldName the name of the field to change * @param fieldValue the value to assign * @return <code>true</code> When the field was changed, false otherwise. */ public boolean setValue(String fieldName, Object fieldValue) { // check the value also when the parent thing is null Object originalValue = values.get(fieldName); // if we have an XML-dbtype field, we always have to store it inside an Element. if(parent != null && getDBType(fieldName) == FieldDefs.TYPE_XML && !(fieldValue instanceof Document)) { log.debug("im called far too often"); Document doc = convertStringToXml(fieldName, (String) fieldValue); if(doc != null) { // store the document inside the field.. much faster... fieldValue = doc; } } // put the key/value in the value hashtable storeValue(fieldName, fieldValue); // process the changed value (?) if (parent != null) { if(!parent.setValue(this,fieldName, originalValue)) { // setValue of parent returned false, no update needed... return false; } } else { log.error("parent was null for node with number" + getNumber()); } setUpdate(fieldName); return true; } /** * Sets a key/value pair in the main values of this node. The value to set is of type <code>boolean</code>. * Note that if this node is a node in cache, the changes are immediately visible to * everyone, even if the changes are not committed. * The fieldName is added to the (public) 'changed' vector to track changes. * @param fieldName the name of the field to change * @param fieldValue the value to assign * @return always <code>true</code> */ public boolean setValue(String fieldName,boolean fieldValue) { return setValue(fieldName,new Boolean(fieldValue)); } /** * Sets a key/value pair in the main values of this node. The value to set is of type <code>int</code>. * Note that if this node is a node in cache, the changes are immediately visible to * everyone, even if the changes are not committed. * The fieldName is added to the (public) 'changed' vector to track changes. * @param fieldName the name of the field to change * @param fieldValue the value to assign * @return always <code>true</code> */ public boolean setValue(String fieldName,int fieldValue) { return setValue(fieldName,new Integer(fieldValue)); } /** * Sets a key/value pair in the main values of this node. The value to set is of type <code>double</code>. * Note that if this node is a node in cache, the changes are immediately visible to * everyone, even if the changes are not committed. * The fieldName is added to the (public) 'changed' vector to track changes. * @param fieldName the name of the field to change * @param fieldValue the value to assign * @return always <code>true</code> */ public boolean setValue(String fieldName,double fieldValue) { return setValue(fieldName,new Double(fieldValue)); } /** * Sets a key/value pair in the main values of this node. * The value to set is converted to the indicated type. * Note that if this node is a node in cache, the changes are immediately visible to * everyone, even if the changes are not committed. * The fieldName is added to the (public) 'changed' vector to track changes. * @deprecated This one will be moved/replaced soon... * Testing of db types will be moved to the DB specific classes * @param fieldName the name of the field to change * @param fieldValue the value to assign * @return <code>false</code> if the value is not of the indicated type, <code>true</code> otherwise */ public boolean setValue(String fieldName, int type, String value) { if (type==FieldDefs.TYPE_UNKNOWN) { log.error("MMObjectNode.setValue(): unsupported fieldtype null for field "+fieldName); return false; } switch (type) { case FieldDefs.TYPE_XML: setValue(fieldName, convertStringToXml(fieldName, value)); break; case FieldDefs.TYPE_STRING: setValue( fieldName, value); break; case FieldDefs.TYPE_NODE: case FieldDefs.TYPE_INTEGER: Integer i; try { i = new Integer(value); } catch (NumberFormatException e) { log.error( e.toString() ); log.error(Logging.stackTrace(e)); return false; } setValue( fieldName, i ); break; case FieldDefs.TYPE_FLOAT: Float f; try { f = new Float(value); } catch (NumberFormatException e) { log.error( e.toString() ); log.error(Logging.stackTrace(e)); return false; } setValue( fieldName, f ); break; case FieldDefs.TYPE_LONG: Long l; try { l = new Long(value); } catch (NumberFormatException e) { log.error( e.toString() ); log.error(Logging.stackTrace(e)); return false; } setValue( fieldName, l ); break; case FieldDefs.TYPE_DOUBLE: Double d; try { d = new Double(value); } catch (NumberFormatException e) { log.error( e.toString() ); log.error(Logging.stackTrace(e)); return false; } setValue( fieldName, d ); break; default: log.error("unsupported fieldtype: "+type+" for field "+fieldName); return false; } return true; } // Add the field to update to the changed Vector // private void setUpdate(String fieldName) { // obtain the type of field this is int state=getDBState(fieldName); // add it to the changed vector so we know that we have to update it // on the next commit if (!changed.contains(fieldName) && state==FieldDefs.DBSTATE_PERSISTENT) { changed.addElement(fieldName); } // is it a memory only field ? then send a fieldchange if (state==0) sendFieldChangeSignal(fieldName); } /** * Retrieve an object's number. * In case of a new node that is not committed, this will return -1. * @return the number of the node */ public int getNumber() { return getIntValue("number"); } /** * Retrieve an object's object type. * This is a number (an index in the typedef builer), rather than a name. * @return the object type number of the node */ public int getOType() { return getIntValue("otype"); } /** * Get a value of a certain field. * @param fieldName the name of the field who's data to return * @return the field's value as an <code>Object</code> */ public Object getValue(String fieldName) { // get the value from the values table Object o = retrieveValue(prefix+fieldName); // routine to check for indirect values // this are used for functions for example // its implemented per builder so lets give this // request to our builder if (o==null) return parent.getValue(this,fieldName); // return the found object return o; } /** * Get a value of a certain field. The value is returned as a * String. Non-string values are automatically converted to * String. 'null' is converted to an empty string. * @param fieldName the name of the field who's data to return * @return the field's value as a <code>String</code> */ public String getStringValue(String fieldName) { // try to get the value from the values table String tmp = ""; Object o = getValue(fieldName); if (o!=null) { if (o instanceof byte[]) { tmp = new String((byte[])o); } else if(o instanceof MMObjectNode) { // tmp = ""+((MMObjectNode)o).getNumber(); } else if(o instanceof Document) { // tmp = convertXmlToString(fieldName, (Document) o ); } else { tmp=""+o; } } // check if the object is shorted, shorted means that // because the value can be a large text/blob object its // not loaded into each object when its first obtained // from the database but that we instead out a text $SHORTED // in the field. Only when the field is really used does this // get mapped into a real value. this saves speed and memory // because every blob/text mapping is a extra request to the // database if (tmp.indexOf("$SHORTED")==0) { if (log.isDebugEnabled()) log.debug("getStringValue(): node="+this+" -- fieldName "+fieldName); // obtain the database type so we can check if what // kind of object it is. this have be changed for // multiple database support. int type=getDBType(fieldName); log.debug("getStringValue(): fieldName "+fieldName+" has type "+type); // check if for known mapped types if (type==FieldDefs.TYPE_STRING) { MMObjectBuilder bul; int number=getNumber(); // check if its in a multilevel node (than we have no node number and // XXX:Not needed, since checking takes place in MultiRelations! // Can be dropped if (prefix!=null && prefix.length()>0) { String tmptable=""; int pos=prefix.indexOf('.'); if (pos!=-1) { tmptable=prefix.substring(0,pos); } else { tmptable=prefix; } // number=getNumber(); bul=parent.mmb.getMMObject(tmptable); log.debug("getStringValue(): "+tmptable+":"+number+":"+prefix+":"+fieldName); } else { bul=parent; } // call our builder with the convert request this will probably // map it to the database we are running. String tmp2=bul.getShortedText(fieldName,number); // did we get a result then store it in the values for next use // and return it. // we could in the future also leave it unmapped in the values // or make this programmable per builder ? if (tmp2!=null) { // store the unmapped value (replacing the $SHORTED text) storeValue(prefix+fieldName,tmp2); // return the found and now unmapped value return tmp2; } else { return null; } } } // return the found value return tmp; } /** * @see getXMLValue */ public Document getXMLValue(String fieldName) { Object o = getValue(fieldName); if(getDBType(fieldName)!= FieldDefs.TYPE_XML) { throw new RuntimeException("field was not an xml-field, dont know how i need to convert this to and xml-document"); } if (o == null) { log.warn("Got null value in field " + fieldName); return null; } if (!(o instanceof Document)) { //do conversion from string to Document thing... log.warn("Field " + fieldName + " did not contain a Document, but a " + o.getClass().getName()); o = convertStringToXml(fieldName, getStringValue(fieldName)); if(o != null) { values.put(fieldName, o); } } return (Document) o; } /** * Get a binary value of a certain field. * @param fieldName the name of the field who's data to return * @return the field's value as an <code>byte []</code> (binary/blob field) */ public byte[] getByteValue(String fieldName) { // try to get the value from the values table // it might be using a prefix to allow multilevel // nodes to work (if not duplicate can not be stored) // call below also allows for byte[] type of // formatting functons. Object obj=getValue(fieldName); // well same as with strings we only unmap byte values when // we really use them since they mean a extra request to the // database most of the time. // we signal with a empty byte[] that its not obtained yet. if (obj instanceof byte[]) { // was allready unmapped so return the value return (byte[])obj; } else { byte[] b; if (getDBType(fieldName) == FieldDefs.TYPE_BYTE) { // call our builder with the convert request this will probably // map it to the database we are running. b=parent.getShortedByte(fieldName,getNumber()); if (b == null) { b = new byte[0]; } // we could in the future also leave it unmapped in the values // or make this programmable per builder ? storeValue(prefix+fieldName,b); } else { if (getDBType(fieldName) == FieldDefs.TYPE_STRING) { String s = getStringValue(fieldName); b = s.getBytes(); } else { b = new byte[0]; } } // return the unmapped value return b; } } /** * Get a value of a certain field. * The value is returned as an MMObjectNode. * If the field contains an Numeric value, the method * tries to obtrain the object with that number. * If it is a String, the method tries to obtain the object with * that alias. The only other possible values are those created by * certain virtual fields. * All remaining situations return <code>null</code>. * @param fieldName the name of the field who's data to return * @return the field's value as an <code>int</code> */ public MMObjectNode getNodeValue(String fieldName) { if (fieldName==null || fieldName.equals("number")) return this; MMObjectNode res=null; Object i=getValue(fieldName); if (i instanceof MMObjectNode) { res=(MMObjectNode)i; } else if (i instanceof Number) { res=parent.getNode(((Number)i).intValue()); } else if (i!=null) { res=parent.getNode(i.toString()); } return res; } /** * Get a value of a certain field. * The value is returned as an int value. Values of non-int, numeric fields are converted if possible. * Booelan fields return 0 for false, 1 for true. * String fields are parsed to a number, if possible. * If a value is an MMObjectNode, it's numberfield is returned. * All remaining field values return -1. * @param fieldName the name of the field who's data to return * @return the field's value as an <code>int</code> */ public int getIntValue(String fieldName) { Object i=getValue(fieldName); int res=-1; if (i instanceof MMObjectNode) { res=((MMObjectNode)i).getNumber(); } else if (i instanceof Boolean) { res=((Boolean)i).booleanValue() ? 1 : 0; } else if (i instanceof Number) { res=((Number)i).intValue(); } else if (i!=null) { try { res=Integer.parseInt(""+i); } catch (NumberFormatException e) {} } return res; } /** * Get a value of a certain field. * The value is returned as an boolean value. * If the actual value is numeric, this call returns <code>true</code> * if the value is a positive, non-zero, value. In other words, values '0' * and '-1' are concidered <code>false</code>. * If the value is a string, this call returns <code>true</code> if * the value is "true" or "yes" (case-insensitive). * In all other cases (including calling byte fields), <code>false</code> * is returned. * Note that there is currently no basic MMBase boolean type, but some * <code>excecuteFunction</code> calls may return a Boolean result. * * @param fieldName the name of the field who's data to return * @return the field's value as an <code>int</code> */ public boolean getBooleanValue(String fieldName) { Object b=getValue(fieldName); boolean res=false; if (b instanceof Boolean) { res=((Boolean)b).booleanValue(); } else if (b instanceof Number) { res=((Number)b).intValue()>0; } else if (b instanceof String) { // note: we don't use Boolean.valueOf() because that only captures // the value "true" res= ((String)b).equalsIgnoreCase("true") || ((String)b).equalsIgnoreCase("yes"); // Call MMLanguage, and compare to // the 'localized' values of true or yes. if ((!res) && (parent!=null)) { MMLanguage languages = (MMLanguage)Module.getModule("mmlanguage"); if (languages!=null) { res= ((String)b).equalsIgnoreCase( languages.getFromCoreEnglish("true")) || ((String)b).equalsIgnoreCase( languages.getFromCoreEnglish("yes")); } } } return res; } /** * Get a value of a certain field. * The value is returned as an Integer value. Values of non-Integer, numeric fields are converted if possible. * Booelan fields return 0 for false, 1 for true. * String fields are parsed to a number, if possible. * All remaining field values return -1. * @param fieldName the name of the field who's data to return * @return the field's value as an <code>Integer</code> */ public Integer getIntegerValue(String fieldName) { Object i=getValue(fieldName); int res=-1; if (i instanceof Boolean) { res=((Boolean)i).booleanValue() ? 1 : 0; } else if (i instanceof Number) { res=((Number)i).intValue(); } else if (i!=null) { try { res=Integer.parseInt(""+i); } catch (NumberFormatException e) {} } return new Integer(res); } /** * Get a value of a certain field. * The value is returned as a long value. Values of non-long, numeric fields are converted if possible. * Booelan fields return 0 for false, 1 for true. * String fields are parsed to a number, if possible. * All remaining field values return -1. * @param fieldName the name of the field who's data to return * @return the field's value as a <code>long</code> */ public long getLongValue(String fieldName) { Object i=getValue(fieldName); long res =-1; if (i instanceof Boolean) { res=((Boolean)i).booleanValue() ? 1 : 0; } else if (i instanceof Number) { res=((Number)i).longValue(); } else if (i!=null) { try { res=Long.parseLong(""+i); } catch (NumberFormatException e) {} } return res; } /** * Get a value of a certain field. * The value is returned as a float value. Values of non-float, numeric fields are converted if possible. * Booelan fields return 0 for false, 1 for true. * String fields are parsed to a number, if possible. * All remaining field values return -1. * @param fieldName the name of the field who's data to return * @return the field's value as a <code>float</code> */ public float getFloatValue(String fieldName) { Object i=getValue(fieldName); float res =-1; if (i instanceof Boolean) { res=((Boolean)i).booleanValue() ? 1 : 0; } else if (i instanceof Number) { res=((Number)i).floatValue(); } else if (i!=null) { try { res=Float.parseFloat(""+i); } catch (NumberFormatException e) {} } return res; } /** * Get a value of a certain field. * The value is returned as a double value. Values of non-double, numeric fields are converted if possible. * Booelan fields return 0 for false, 1 for true. * String fields are parsed to a number, if possible. * All remaining field values return -1. * @param fieldName the name of the field who's data to return * @return the field's value as a <code>double</code> */ public double getDoubleValue(String fieldName) { Object i=getValue(fieldName); double res =-1; if (i instanceof Boolean) { res=((Boolean)i).booleanValue() ? 1 : 0; } else if (i instanceof Number) { res=((Number)i).doubleValue(); } else if (i!=null) { try { res=Double.parseDouble(""+i); } catch (NumberFormatException e) {} } return res; } /** * Returns the DBType of a field. * @param fieldName the name of the field who's type to return * @return the field's DBType */ public int getDBType(String fieldName) { if (prefix!=null && prefix.length()>0) { // If the prefix is set use the builder contained therein int pos=prefix.indexOf('.'); if (pos==-1) pos=prefix.length(); MMObjectBuilder bul=parent.mmb.getMMObject(prefix.substring(0,pos)); return bul.getDBType(fieldName); } else { return parent.getDBType(fieldName); } } /** * Returns the DBState of a field. * @param fieldName the name of the field who's state to return * @return the field's DBState */ public int getDBState(String fieldName) { if (parent!=null) { return parent.getDBState(fieldName); } else { return FieldDefs.DBSTATE_UNKNOWN; } } /** * Return all the names of fields that were changed. * Note that this is a direct reference. Changes (i.e. clearing the vector) will affect the node's status. * @param a <code>Vector</code> containing all the fieldNames */ public Vector getChanged() { return changed; } /** * Tests whether one of the values of this node was changed since the last commit/insert. * @return <code>true</code> if changes have been made, <code>false</code> otherwise */ public boolean isChanged() { if (changed.size()>0) { return true; } else { return false; } } /** * Clear the 'signal' Vector with the changed keys since last commit/insert. * Marks the node as 'unchanged'. * Does not affect the values of the fields, nor does it commit the node. * @return always <code>true</code> */ public boolean clearChanged() { changed=new Vector(); return true; } /** * Return the values of this node as a hashtable (name-value pair). * Note that this is a direct reference. Changes will affect the node. * Used by various export routines. * @return the values as a <code>Hashtable</code> */ public Hashtable getValues() { return values; } /** * Deletes the propertie cache for this node. * Forces a reload of the properties on next use. */ public void delPropertiesCache() { synchronized(properties_sync) { properties=null; } } /** * Return a the properties for this node. * @return the properties as a <code>Hashtable</code> */ public Hashtable getProperties() { synchronized(properties_sync) { if (properties==null) { properties=new Hashtable(); MMObjectBuilder bul=parent.mmb.getMMObject("properties"); Enumeration e=bul.search("parent=="+getNumber()); while (e.hasMoreElements()) { MMObjectNode pnode=(MMObjectNode)e.nextElement(); String key=pnode.getStringValue("key"); properties.put(key,pnode); } } } return properties; } /** * Returns a specified property of this node. * @param key the name of the property to retrieve * @return the property object as a <code>MMObjectNode</code> */ public MMObjectNode getProperty(String key) { MMObjectNode n; synchronized(properties_sync) { if (properties==null) { getProperties(); } n=(MMObjectNode)properties.get(key); } if (n!=null) { return n; } else { return null; } } /** * Sets a specified property for this node. * This method does not commit anything - it merely updates the node's propertylist. * @param node the property object as a <code>MMObjectNode</code> */ public void putProperty(MMObjectNode node) { synchronized(properties_sync) { if (properties==null) { getProperties(); } properties.put(node.getStringValue("key"),node); } } /** * Return the GUI indicator for this node. * The GUI indicator is a string that represents the contents of this node. * By default it is the string-representation of the first non-system field of the node. * Individual builders can alter this behavior. * @return the GUI iddicator as a <code>String</code> */ public String getGUIIndicator() { if (parent!=null) { return parent.getGUIIndicator(this); } else { log.error("MMObjectNode -> can't get parent"); return "problem"; } } /** * Return the buildername of this node * @return the builder table name */ public String getName() { return parent.getTableName(); } /** * Delete the relation cache for this node. * This means it will be reloaded from the database/storage on next use. */ public void delRelationsCache() { relations=null; } /** * Returns whether this node has relations. * This includes unidirection relations which would otherwise not be counted. * @return <code>true</code> if any relations exist, <code>false</code> otherwise. */ public boolean hasRelations() { // return getRelationCount()>0; return parent.mmb.getInsRel().hasRelations(getNumber()); } /** * Return all the relations of this node. * Use only to delete the relations of a node. * Note that this returns the nodes describing the relation - not the nodes 'related to'. * @return An <code>Enumeration</code> containing the nodes */ public Enumeration getAllRelations() { Vector allrelations=parent.mmb.getInsRel().getAllRelationsVector(getNumber()); if (allrelations!=null) { return allrelations.elements(); } else { return null; } } /** * Return the relations of this node. * Note that this returns the nodes describing the relation - not the nodes 'related to'. * @return An <code>Enumeration</code> containing the nodes */ public Enumeration getRelations() { if (relations==null) { relations=parent.getRelations_main(getNumber()); relation_cache_miss++; } else { relation_cache_hits++; } if (relations!=null) { return relations.elements(); } else { return null; } } /** * Remove the relations of the node. */ public void removeRelations() { parent.removeRelations(this); } /** * Returns the number of relations of this node. * @return An <code>int</code> indicating the number of nodes found */ public int getRelationCount() { if (relations==null) { relations=parent.getRelations_main(getNumber()); relation_cache_miss++; } else { relation_cache_hits++; } if (relations!=null) { return relations.size(); } else { return 0; } } /** * Return the relations of this node, filtered on a specified type. * Note that this returns the nodes describing the relation - not the nodes 'related to'. * @param otype the 'type' of relations to return. The type identifies a relation (InsRel-derived) builder, not a reldef object. * @return An <code>Enumeration</code> containing the nodes */ public Enumeration getRelations(int otype) { Enumeration e = getRelations(); Vector result=new Vector(); if (e!=null) { while (e.hasMoreElements()) { MMObjectNode tnode=(MMObjectNode)e.nextElement(); if (tnode.getOType()==otype) { result.addElement(tnode); } } } return result.elements(); } /** * Return the relations of this node, filtered on a specified type. * Note that this returns the nodes describing the relation - not the nodes 'related to'. * @param wantedtype the 'type' of relations to return. The type identifies a relation (InsRel-derived) builder, not a reldef object. * @return An <code>Enumeration</code> containing the nodes */ public Enumeration getRelations(String wantedtype) { int otype=parent.mmb.getTypeDef().getIntValue(wantedtype); if (otype!=-1) { return getRelations(otype); } return null; } /** * Return the number of relations of this node, filtered on a specified type. * @param wantedtype the 'type' of related nodes (NOT the relations!). * @return An <code>int</code> indicating the number of nodes found */ public int getRelationCount(String wantedtype) { int count=0; int otype=parent.mmb.getTypeDef().getIntValue(wantedtype); if (otype!=-1) { if (relations==null) { relations=parent.mmb.getInsRel().getRelationsVector(getNumber()); relation_cache_miss++; } else { relation_cache_hits++; } if (relations!=null) { for(Enumeration e=relations.elements();e.hasMoreElements();) { MMObjectNode tnode=(MMObjectNode)e.nextElement(); int snumber=tnode.getIntValue("snumber"); int nodetype =0; if (snumber==getNumber()) { nodetype=parent.getNodeType(tnode.getIntValue("dnumber")); } else { nodetype=parent.getNodeType(snumber); } if (nodetype==otype) { count +=1; } } } } else { log.warn("getRelationCount is requested with an invalid Builder name (otype "+wantedtype+" does not exist)"); } return count; } /** * Returns the node's age * @return the age in days */ public int getAge() { return parent.getAge(this); } /** * Returns the node's builder tablename. * @return the tablename of the builder as a <code>String</code> * @deprecated use getName instead */ public String getTableName() { return parent.getTableName(); } /** * Sends a field-changed signal. * @param fieldName the name of the changed field. * @return always <code>true</code> */ public boolean sendFieldChangeSignal(String fieldName) { return parent.sendFieldChangeSignal(this,fieldName); } /** * Sets the node's alias. * The code only sets a (memory) property, it does not actually add the alias to the database. * Does not support multiple aliases. */ public void setAlias(String alias) { this.alias=alias; } /** * Returns the node's alias. * Does not support multiple aliases. * @return the alias as a <code>String</code> */ public String getAlias() { return alias; } /** * Get all related nodes. The returned nodes are not the * nodes directly attached to this node (the relation nodes) but the nodes * attached to the relation nodes of this node. * @return a <code>Vector</code> containing <code>MMObjectNode</code>s */ public Vector getRelatedNodesOld() { Vector result = new Vector(); for (Enumeration e = getRelations(); e.hasMoreElements();) { MMObjectNode relNode = (MMObjectNode)e.nextElement(); int number = relNode.getIntValue("dnumber"); if (number == getNumber()) { number = relNode.getIntValue("snumber"); } MMObjectNode destNode = (MMObjectNode)parent.getNode(number); result.addElement(destNode); } return result; } /** * Get all related nodes. The returned nodes are not the * nodes directly attached to this node (the relation nodes) but the nodes * attached to the relation nodes of this node. * @return a <code>Vector</code> containing <code>MMObjectNode</code>s */ public Vector getRelatedNodes() { Vector result = new Vector(); String type ="object"; MMObjectBuilder builder = (MMObjectBuilder)parent.mmb.getMMObject(type); // example: we want a thisnode.relatedNodes(mediaparts) where mediaparts are of type // audioparts and videoparts. This method will return the real nodes (thus of type audio/videoparts) // when asked to get nodes of type mediaparts. // // - get a list of virtual nodes from a multilevel("this.parent.name, type") ordered on otype // (this will return virtual audio- and/or videoparts ordered on their *real* parent) // - construct a list of nodes for each parentbuilder seperately // - ask the parentbuilder for each list of virtual nodes to get a list of the real nodes // 'object' is not a valid builder, but it is accepted in this query if( builder != null || type.equals("object")) { // multilevel from table this.parent.name -> type Vector tables = new Vector(); tables.addElement(parent.getTableName()); tables.addElement(type); // return type.number (and otype for sorting) Vector fields = new Vector(); fields.addElement(type + ".number"); fields.addElement(type + ".otype"); // order list UP Vector directions = new Vector(); directions.addElement("UP"); // and order on otype Vector ordered = new Vector(); ordered.addElement(type + ".otype"); // retrieve the related nodes (these are virtual) Vector v = multirelations.searchMultiLevelVector( getNumber(),fields,"NO",tables,"",ordered,directions); result = new Vector(getRealNodes(v, type)); } else { log.error("This type("+type+") is not a valid buildername!"); } log.debug("related("+parent.getTableName()+"("+getNumber()+")) = size("+result.size()+")"); return result; } /** * Get the related nodes of a certain type. The returned nodes are not the * nodes directly attached to this node (the relation nodes) but the nodes * attached to the relation nodes of this node. * * @param type the type of objects to be returned * @return a <code>Vector</code> containing <code>MMObjectNode</code>s */ public Vector getRelatedNodes(String type) { Vector result = new Vector(); MMObjectBuilder builder = (MMObjectBuilder)parent.mmb.getMMObject(type); // example: we want a thisnode.relatedNodes(mediaparts) where mediaparts are of type // audioparts and videoparts. This method will return the real nodes (thus of type audio/videoparts) // when asked to get nodes of type mediaparts. // // - get a list of virtual nodes from a multilevel("this.parent.name, type") ordered on otype // (this will return virtual audio- and/or videoparts ordered on their *real* parent) // - construct a list of nodes for each parentbuilder seperately // - ask the parentbuilder for each list of virtual nodes to get a list of the real nodes if( builder != null ) { // multilevel from table this.parent.name -> type Vector tables = new Vector(); tables.addElement(parent.getTableName()); tables.addElement(type); // return type.number (and otype for sorting) Vector fields = new Vector(); fields.addElement(type + ".number"); fields.addElement(type + ".otype"); // order list UP Vector directions = new Vector(); directions.addElement("UP"); // and order on otype Vector ordered = new Vector(); ordered.addElement(type + ".otype"); // retrieve the related nodes (these are virtual) Vector v = multirelations.searchMultiLevelVector( getNumber(),fields,"NO",tables,"",ordered,directions); result = new Vector(getRealNodes(v, type)); } else { log.error("This type("+type+") is not a valid buildername!"); } log.debug("related("+parent.getTableName()+"("+getNumber()+")) -> "+type+" = size("+result.size()+")"); return result; } /** * Loop through the virtuals vector, group all same nodes based on parent and fetch the real nodes from those parents * * @param Vector of virtual nodes (only type.number and type.otype fields are set) * @param type, needed to retreive the otype, which is set in node as type + ".otype" * @returns List of real nodes * * @see getRelatedNodes(String type) */ private List getRealNodes(Vector virtuals, String type) { List result = new ArrayList(); MMObjectBuilder rparent = null; MMObjectNode node = null; MMObjectNode convert = null; Enumeration e = virtuals.elements(); List list = new ArrayList(); int otype = -1; int ootype = -1; // fill the list while(e.hasMoreElements()) { node = (MMObjectNode)e.nextElement(); otype = node.getIntValue(type + ".otype"); // convert the nodes of type ootype to real numbers if(otype != ootype) { // if we have nodes return real values if(ootype != -1) { result.addAll(getRealNodesFromBuilder(list, ootype)); list = new ArrayList(); } ootype = otype; } // convert current node type.number and type.otype to number and otype convert = new MMObjectNode(); // parent needs to be set or else mmbase does nag nag nag on a setValue() convert.parent = parent.mmb.getMMObject(parent.mmb.TypeDef.getValue(otype)); convert.setValue("number", node.getValue(type + ".number")); convert.setValue("otype", otype); list.add(convert); // first and only list or last list, return real values if(!e.hasMoreElements()) { // log.debug("subconverting last "+list.size()+" nodes of type("+otype+")"); result.addAll(getRealNodesFromBuilder(list, otype)); } } // check that we didnt loose any nodes // Java 1.4 // assert(virtuals.size() == result.size()); // Below Java 1.4 if(virtuals.size() != result.size()) { log.error("We lost a few nodes during conversion from virtualnodes("+virtuals.size()+") to realnodes("+result.size()+")"); } return result; } private List getRealNodesFromBuilder(List list, int otype) { List result = new ArrayList(); String name = parent.mmb.TypeDef.getValue(otype); if(name != null) { MMObjectBuilder rparent = parent.mmb.getMMObject(name); if(rparent != null) { result.addAll(rparent.getNodes(list)); } else { log.error("This otype("+otype+") does not denote a valid typedef-name("+name+")!"); } } else { log.error("This otype("+otype+") gives no name("+name+") from typedef!"); } return result; } public static int getRelationCacheHits() { return relation_cache_hits; } public static int getRelationCacheMiss() { return relation_cache_miss; } /** * Convert a String value of a field to a Document * @param fieldName The field to be used. * @param value The current value of the field, (can be null) * @return A DOM <code>Document</code> or <code>null</code> if there was no value and builder allowed to be null * @throws RuntimeException When value was null and not allowed by builer, and xml failures. */ private Document convertStringToXml(String fieldName, String value) { value = value.trim(); if(value == null || value.length()==0) { log.debug("field was empty"); // may only happen, if the field may be null... if(parent.getField(fieldName).getDBNotNull()) { throw new RuntimeException("field with name '"+fieldName+"' may not be null"); } return null; } if (value.startsWith("<")) { // removing doc-headers if nessecary // remove all the <?xml stuff from beginning if there.... // <?xml version="1.0" encoding="utf-8"?> if(value.startsWith("<?xml")) { // strip till next ?> int stop = value.indexOf("?>"); if(stop > 0) { value = value.substring(stop + 2).trim(); log.debug("removed <?xml part"); } else { throw new RuntimeException("no ending ?> found in xml:\n" + value); } } else { log.debug("no <?xml header found"); } // remove all the <!DOCTYPE stuff from beginning if there.... // <!DOCTYPE builder PUBLIC "-//MMBase/builder config 1.0 //EN" "http://www.mmbase.org/dtd/builder_1_1.dtd"> if(value.startsWith("<!DOCTYPE")) { // strip till next > int stop = value.indexOf(">"); if(stop > 0) { value = value.substring(stop + 1).trim(); log.debug("removed <!DOCTYPE part"); } else { throw new RuntimeException("no ending > found in xml:\n" + value); } } else { log.debug("no <!DOCTYPE header found"); } } else { // not XML, make it XML, when conversion specified, use it... String propertyName = fieldName + ".xmlconversion"; String conversion = parent.getInitParameter(propertyName); if(conversion == null) { conversion = "MMXF_POOR"; log.warn("property: '"+propertyName+"' for builder: '"+parent.getTableName()+"' was not set, converting string to xml for field: '" + fieldName + "' using the default: '" + conversion + "'."); } log.debug("converting the string to something else using conversion: " + conversion); value = org.mmbase.util.Encode.decode(conversion, (String) value); } if (log.isDebugEnabled()) { log.trace("using xml string:\n"+value); } // add the header stuff... String xmlHeader = "<?xml version=\"1.0\" encoding=\"" + parent.mmb.getEncoding() + "\" ?>"; String doctype = parent.getField(fieldName).getDBDocType(); if(doctype != null) { xmlHeader += "\n" + doctype; } value = xmlHeader + "\n" + value; ///////////////////////////////////////////// // TODO: RE-USE THE PARSER EVERY TIME ! // try { // getXML also uses a documentBuilder, maybe we can speed it up by making it a static member variable,, // or ask it from BasicReader ? javax.xml.parsers.DocumentBuilderFactory dfactory = javax.xml.parsers.DocumentBuilderFactory.newInstance(); if(doctype != null) { log.debug("validating the xmlfield for field with name:" + fieldName + " with doctype: " + doctype); dfactory.setValidating(true); } javax.xml.parsers.DocumentBuilder documentBuilder = dfactory.newDocumentBuilder(); // dont log errors, and try to process as much as possible... org.mmbase.util.XMLErrorHandler errorHandler = new org.mmbase.util.XMLErrorHandler(false, org.mmbase.util.XMLErrorHandler.NEVER); documentBuilder.setErrorHandler(errorHandler); documentBuilder.setEntityResolver(new org.mmbase.util.XMLEntityResolver()); // ByteArrayInputStream? // Yes, in contradiction to what one would think, XML are bytes, rather then characters. Document doc = documentBuilder.parse(new java.io.ByteArrayInputStream(value.getBytes(parent.mmb.getEncoding()))); if(!errorHandler.foundNothing()) { throw new RuntimeException("xml for field with name: '"+fieldName+"' invalid:\n"+errorHandler.getMessageBuffer()+"for xml:\n"+value); } return doc; } catch(javax.xml.parsers.ParserConfigurationException pce) { String msg = "[sax parser] not well formed xml: "+pce.toString() + " node#"+getNumber()+"\n"+value+"\n" + Logging.stackTrace(pce); log.error(msg); throw new RuntimeException(msg); } catch(org.xml.sax.SAXException se) { String msg = "[sax] not well formed xml: "+se.toString() + "("+se.getMessage()+")" + " node#"+getNumber()+"\n"+value+"\n" + Logging.stackTrace(se); log.error(msg); throw new RuntimeException(msg); } catch(java.io.IOException ioe) { String msg = "[io] not well formed xml: "+ioe.toString() + " node#"+getNumber()+"\n"+value+"\n" + Logging.stackTrace(ioe); log.error(msg); throw new RuntimeException(msg); } } private String convertXmlToString(String fieldName, Document xml) { log.debug("converting from xml to string"); // check for null values if(xml == null) { log.debug("field was empty"); // string with null isnt allowed in mmbase... return ""; } // check if we are using the right DOC-type for this field.... String doctype = parent.getField(fieldName).getDBDocType(); if(doctype != null) { // we have a doctype... the doctype of the document has to mach the doctype of the doctype which is needed.. org.w3c.dom.DocumentType type = xml.getDoctype(); String publicId = type.getPublicId(); if(doctype.indexOf(publicId) == -1) { throw new RuntimeException("doctype('"+doctype+"') required by field '"+fieldName+"' and public id was NOT in it : '"+publicId+"'"); } log.warn("doctype check can not completely be trusted"); } ///////////////////////////////////////////// // TODO: RE-USE THE PARSER EVERY TIME ! // try { // getXML also uses a documentBuilder, maybe we can speed it up by making it a static member variable,, // or ask it from BasicReader ? //make a string from the XML javax.xml.transform.TransformerFactory tfactory = javax.xml.transform.TransformerFactory.newInstance(); //tfactory.setURIResolver(new org.mmbase.util.xml.URIResolver(new java.io.File(""))); javax.xml.transform.Transformer serializer = tfactory.newTransformer(); // for now, we save everything in ident form, this since it makes debugging a little bit more handy serializer.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes"); // store as less as possible, otherthings should be resolved from gui-type serializer.setOutputProperty(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); java.io.StringWriter str = new java.io.StringWriter(); serializer.transform(new javax.xml.transform.dom.DOMSource(xml), new javax.xml.transform.stream.StreamResult(str)); if (log.isDebugEnabled()) { log.debug("xml -> string:\n" + str.toString()); } return str.toString(); } catch(javax.xml.transform.TransformerConfigurationException tce) { String message = tce.toString() + " " + Logging.stackTrace(tce); log.error(message); throw new RuntimeException(message); } catch(javax.xml.transform.TransformerException te) { String message = te.toString() + " " + Logging.stackTrace(te); log.error(message); throw new RuntimeException(message); } } }
