0001: /*
0002:
0003: This software is OSI Certified Open Source Software.
0004: OSI Certified is a certification mark of the Open Source Initiative.
0005:
0006: The license (Mozilla version 1.0) can be read at the MMBase site.
0007: See http://www.MMBase.org/license
0008:
0009: */
0010: package org.mmbase.util.xml;
0011:
0012: import java.util.*;
0013:
0014: import org.w3c.dom.*;
0015: import org.xml.sax.InputSource;
0016: import org.mmbase.bridge.Field;
0017: import org.mmbase.bridge.NodeManager;
0018: import org.mmbase.core.CoreField;
0019: import org.mmbase.core.util.Fields;
0020: import org.mmbase.datatypes.*;
0021: import org.mmbase.datatypes.util.xml.DataTypeReader;
0022: import org.mmbase.datatypes.util.xml.DependencyException;
0023: import org.mmbase.module.core.MMBase;
0024: import org.mmbase.module.core.MMObjectBuilder;
0025: import org.mmbase.storage.util.Index;
0026:
0027: import org.mmbase.util.LocalizedString;
0028: import org.mmbase.util.XMLEntityResolver;
0029: import org.mmbase.util.functions.*;
0030: import org.mmbase.util.logging.*;
0031:
0032: /**
0033: * Used to parse and retrieve data from a builder configuration file.
0034: * The parser support builders for builder dtd 1.1.
0035: *
0036: * @since MMBase 1.7
0037: * @author Case Roole
0038: * @author Rico Jansen
0039: * @author Pierre van Rooden
0040: * @author Michiel Meeuwissen
0041: * @version $Id: BuilderReader.java,v 1.97 2008/03/12 15:21:43 michiel Exp $
0042: */
0043: public class BuilderReader extends DocumentReader {
0044:
0045: /** Public ID of the Builder DTD version 1.0 */
0046: public static final String PUBLIC_ID_BUILDER_1_0 = "-//MMBase//DTD builder config 1.0//EN";
0047: /** Public ID of the Builder DTD version 1.1 */
0048: public static final String PUBLIC_ID_BUILDER_1_1 = "-//MMBase//DTD builder config 1.1//EN";
0049:
0050: // deprecated builder dtds
0051: private static final String PUBLIC_ID_BUILDER_1_0_FAULT = "-//MMBase/DTD builder config 1.0//EN";
0052: private static final String PUBLIC_ID_BUILDER_OLD = "/MMBase - builder//";
0053: private static final String PUBLIC_ID_BUILDER_1_1_FAULT = "-//MMBase/DTD builder config 1.1//EN";
0054:
0055: /** DTD resource filename of the Builder DTD version 1.0 */
0056: public static final String DTD_BUILDER_1_0 = "builder_1_0.dtd";
0057: /** DTD resource filename of the Builder DTD version 1.1 */
0058: public static final String DTD_BUILDER_1_1 = "builder_1_1.dtd";
0059:
0060: /** Public ID of the most recent Builder DTD */
0061: public static final String PUBLIC_ID_BUILDER = PUBLIC_ID_BUILDER_1_1;
0062: /** DTD respource filename of the most recent Builder DTD */
0063: public static final String DTD_BUILDER = DTD_BUILDER_1_1;
0064:
0065: public static final String XSD_BUILDER_2_0 = "builder.xsd";
0066: public static final String NAMESPACE_BUILDER_2_0 = "http://www.mmbase.org/xmlns/builder";
0067: public static final String NAMESPACE_BUILDER = NAMESPACE_BUILDER_2_0;
0068:
0069: private static final Logger log = Logging
0070: .getLoggerInstance(BuilderReader.class);
0071:
0072: /**
0073: * Register the namespace and XSD used by DataTypeConfigurer
0074: * This method is called by XMLEntityResolver.
0075: */
0076: public static void registerSystemIDs() {
0077: XMLEntityResolver.registerSystemID(NAMESPACE_BUILDER_2_0
0078: + ".xsd", XSD_BUILDER_2_0, BuilderReader.class);
0079: }
0080:
0081: /**
0082: * Register the Public Ids for DTDs used by BuilderReader
0083: * This method is called by XMLEntityResolver.
0084: */
0085: public static void registerPublicIDs() {
0086: // various builder dtd versions
0087: XMLEntityResolver.registerPublicID(PUBLIC_ID_BUILDER_1_0,
0088: DTD_BUILDER_1_0, BuilderReader.class);
0089: XMLEntityResolver.registerPublicID(PUBLIC_ID_BUILDER_1_1,
0090: DTD_BUILDER_1_1, BuilderReader.class);
0091: //XMLEntityResolver.registerPublicID("-//MMBase//DTD builder config 2.0//EN", "builder_2_0.dtd", BuilderReader.class);
0092:
0093: // legacy public IDs (wrong, don't use these)
0094: XMLEntityResolver.registerPublicID(PUBLIC_ID_BUILDER_1_0_FAULT,
0095: DTD_BUILDER_1_0, BuilderReader.class);
0096: XMLEntityResolver.registerPublicID(PUBLIC_ID_BUILDER_OLD,
0097: DTD_BUILDER_1_0, BuilderReader.class);
0098: XMLEntityResolver.registerPublicID(PUBLIC_ID_BUILDER_1_1_FAULT,
0099: DTD_BUILDER_1_1, BuilderReader.class);
0100: }
0101:
0102: /**
0103: * MMBase instance, used to load parent (extending) builders
0104: */
0105: private MMBase mmbase;
0106:
0107: /**
0108: * Parent builder.
0109: * If assigned, the properties of this builder are used as 'defaults'
0110: * and the fields of the builder are inherited.
0111: * @since MMbase-1.6
0112: */
0113: private MMObjectBuilder parentBuilder;
0114:
0115: /**
0116: * If false, the parent builder could not be resolved.
0117: * A builder with an unresolved parent is set to 'inactive', regardless of actual status
0118: * The default value is false, as resolving Inheritance is mandatory when loading builders.
0119: * @since MMbase-1.6
0120: */
0121: private boolean inheritanceResolved = false;
0122:
0123: /**
0124: * searchPositions and inputPositions are used to adminstrate 'occupied' positions (from
0125: * editor/positions), which is used to find defaults if not specified.
0126: * @since MMBase-1.7
0127: */
0128: private SortedSet<Integer> searchPositions = new TreeSet<Integer>();
0129: private SortedSet<Integer> inputPositions = new TreeSet<Integer>();
0130:
0131: /**
0132: * @since MMBase-1.7
0133: */
0134: public BuilderReader(InputSource source, MMBase mmb) {
0135: super (source, BuilderReader.class);
0136: mmbase = mmb;
0137: if (getRootElement().getTagName().equals("builder")) {
0138: resolveInheritance();
0139: }
0140: }
0141:
0142: /**
0143: * @since MMBase-1.8
0144: */
0145: public BuilderReader(Document doc, MMBase mmb) {
0146: super (doc);
0147: mmbase = mmb;
0148: if (getRootElement().getTagName().equals("builder")) {
0149: resolveInheritance();
0150: }
0151: }
0152:
0153: /**
0154: * Resolves inheritance.
0155: * If a builder 'extends' another builder, the parser attempts to
0156: * retrieve a reference to this builder (using getParentBuilder).
0157: * Note that if inheritance cannot be resolved, the builder cannot be activated.
0158: * This method returns false if the builder to extend from is inactive.
0159: * It throws a RuntimeException is the builder to extend from is not allowed as
0160: * an parent builder.
0161: *
0162: * @since MMBase-1.6
0163: * @return true if inheritance could be resolved, false if the .
0164: * @see #isInheritanceResolved()
0165: * @throws RuntimeException when the builder to extend from is not allowed as parent
0166: */
0167: protected boolean resolveInheritance() {
0168: String buildername = getExtends();
0169: if (buildername.equals("")) {
0170: parentBuilder = null;
0171: inheritanceResolved = true;
0172: } else {
0173: inheritanceResolved = false;
0174: if (mmbase != null) {
0175: parentBuilder = mmbase.getBuilder(buildername);
0176: inheritanceResolved = (parentBuilder != null);
0177: if (inheritanceResolved) { // fill inputPositions, searchPositions
0178: Iterator<CoreField> fields = parentBuilder
0179: .getFields(NodeManager.ORDER_EDIT)
0180: .iterator();
0181: while (fields.hasNext()) {
0182: CoreField def = fields.next();
0183: inputPositions.add(def.getEditPosition());
0184: }
0185: fields = parentBuilder.getFields(
0186: NodeManager.ORDER_SEARCH).iterator();
0187: while (fields.hasNext()) {
0188: CoreField def = fields.next();
0189: searchPositions.add(def.getSearchPosition());
0190: }
0191: }
0192: }
0193: }
0194: return inheritanceResolved;
0195: }
0196:
0197: /**
0198: * Detremines if inheritance is resolved.
0199: * This method returns true if a call to resolveInheritance succeeded.
0200: * it returns false if resolveInheritance failed (returned false or threw an exception)
0201: *
0202: * @since MMBase-1.6
0203: * @return true if inheritance could be resolved
0204: * @see #resolveInheritance()
0205: */
0206: public boolean isInheritanceResolved() {
0207: return inheritanceResolved;
0208: }
0209:
0210: /**
0211: * Get the status of this builder.
0212: * Note that if inheritance cannot be resolved, this method always returns "inactive".
0213: * @return a String decribing the status ("active" or "inactive")
0214: */
0215: public String getStatus() {
0216: if (!inheritanceResolved) {
0217: return "inactive"; // extends an inactive or non-existing builder
0218: } else {
0219: String val = getElementValue("builder.status")
0220: .toLowerCase();
0221: if (!val.equals("inactive")) {
0222: val = "active"; // fix invalid values, including empty value, in which case
0223: // assume it extends an active builder (i.e. object)
0224: }
0225: return val;
0226: }
0227: }
0228:
0229: /**
0230: * Retrieves the Search Age.
0231: * The search age may be used by editors or search forms to determine
0232: * the maximum age in days of an object to be searched (limiting the resultset
0233: * of a search)
0234: * @return the search age in days
0235: */
0236: public int getSearchAge() {
0237: int val = 30;
0238: String sval = getElementValue("builder.searchage");
0239: if (sval.equals("") && (parentBuilder != null)) {
0240: sval = parentBuilder.getSearchAge();
0241: }
0242: try {
0243: val = Integer.parseInt(sval);
0244: } catch (Exception f) {
0245: }
0246: return val;
0247: }
0248:
0249: /**
0250: * Get the class name to use for instantiating this builder.
0251: * Note that it is possible to specify a short-hand format in
0252: * the builder configuration file.
0253: * If only the classname (withoput package name) is given, the classname
0254: * is expanded to fall into the <code>org.mmbase.module.builders</code> package.
0255: * @return the classname to use.
0256: */
0257: public String getClassName() {
0258: String val = getElementValue("builder.class");
0259: if (val.equals("")) {
0260: val = getElementValue("builder.classfile");// deprecated!! (makes no sense, it is no file)
0261: }
0262:
0263: if (val.equals("")) {
0264: if (parentBuilder != null) {
0265: return parentBuilder.getClass().getName();
0266: } else {
0267: return "";
0268: }
0269: }
0270: // is it a full name or inside the org.mmbase.module.builders.* path
0271: int pos = val.indexOf('.');
0272: if (pos == -1) {
0273: val = "org.mmbase.module.builders." + val;
0274: }
0275: if ("org.mmbase.module.corebuilders.ObjectTypes".equals(val)) {
0276: log
0277: .warn("Specified the removed builder 'ObjectTypes', fall back to TypeDef. You can remove all core-builders from your configuration directory (the ones present in mmbase.jar are ok)");
0278: val = "org.mmbase.module.corebuilders.TypeDef";
0279: }
0280: return val;
0281: }
0282:
0283: /**
0284: * Get the datatypes defined for this builder.
0285: * @param collector A DataTypeCollector to which the newly found DataTypes will be added.
0286: * @return Returns the data-types of the given collector after adding the ones which are configured
0287: * @since MMBase-1.8
0288: */
0289: public Map<String, BasicDataType<?>> getDataTypes(
0290: DataTypeCollector collector) {
0291: Element element = getElementByPath("builder.datatypes");
0292: if (element != null) {
0293: DataTypeReader.readDataTypes(element, collector);
0294: }
0295: return collector.getDataTypes();
0296: }
0297:
0298: /**
0299: * Get the field definitions of this builder.
0300: * If applicable, this includes the fields inherited from a parent builder.
0301: *
0302: * @return a List of all Fields as CoreField
0303: * @since MMBase-1.8
0304: */
0305: public List<CoreField> getFields() {
0306: return getFields(null, DataTypes.getSystemCollector());
0307: }
0308:
0309: /**
0310: * Get the field definitions of this builder.
0311: * If applicable, this includes the fields inherited from a parent builder.
0312: *
0313: * @param builder the MMObjectBuilder to which the fields will be added
0314: * @param collector the datatype collector used to access the datatypes available for the fields to read.
0315: * @return a List of all Fields as CoreField
0316: * @since MMBase-1.8
0317: */
0318: public List<CoreField> getFields(MMObjectBuilder builder,
0319: DataTypeCollector collector) {
0320: List<CoreField> results = new ArrayList<CoreField>();
0321: Map<String, CoreField> oldset = new HashMap<String, CoreField>();
0322: int pos = 1;
0323: if (parentBuilder != null) {
0324: List<CoreField> parentfields = parentBuilder
0325: .getFields(NodeManager.ORDER_CREATE);
0326: if (parentfields != null) {
0327: // have to clone the parent fields
0328: // need clone()!
0329: for (CoreField f : parentfields) {
0330: CoreField newField = (CoreField) f.clone(f
0331: .getName());
0332: newField.setParent(builder);
0333: while (newField.getStoragePosition() >= pos)
0334: pos++;
0335: newField.finish();
0336: results.add(newField);
0337: oldset.put(newField.getName(), newField);
0338: }
0339: }
0340: }
0341:
0342: for (Element fieldList : getChildElements("builder",
0343: "fieldlist")) {
0344: for (Element field : getChildElements(fieldList, "field")) {
0345: String fieldName = getElementAttributeValue(field,
0346: "name");
0347: if ("".equals(fieldName)) {
0348: fieldName = getElementValue(getElementByPath(field,
0349: "field.db.name"));
0350: }
0351: CoreField def = oldset.get(fieldName);
0352: try {
0353: if (def != null) {
0354: def.rewrite();
0355: DataType dataType = decodeDataType(builder,
0356: collector, def.getName(), field, def
0357: .getType(), def
0358: .getListItemType(), false);
0359: if (dataType != null) {
0360: def.setDataType(dataType); // replace datatype
0361: }
0362: decodeFieldDef(field, def, collector);
0363: decodeFieldAttributes(field, def);
0364: def.finish();
0365: } else {
0366: def = decodeFieldDef(builder, collector, field);
0367: def.setStoragePosition(pos++);
0368: def.finish();
0369: results.add(def);
0370: }
0371: } catch (Exception e) {
0372: log.error("During parsing of "
0373: + XMLWriter.write(field, true, true) + " "
0374: + e.getMessage(), e);
0375: }
0376: }
0377: }
0378: return results;
0379: }
0380:
0381: /**
0382: * Get the named indices of this builder.
0383: * Note that the 'default' index (set with the 'key' attribute) is also included
0384: * in this list (with the name {@link Index#MAIN}).
0385: *
0386: * @param builder the MMObjectBuilder to which the fields will be added
0387: * @return a List of all Indices
0388: */
0389: public List<Index> getIndices(MMObjectBuilder builder) {
0390: List<Index> results = new ArrayList<Index>();
0391: Index mainIndex = null;
0392: if (parentBuilder != null) {
0393: // create the
0394: Index parentIndex = parentBuilder.getStorageConnector()
0395: .getIndex(Index.MAIN);
0396: if (parentIndex != null) {
0397: mainIndex = new Index(builder, Index.MAIN);
0398: mainIndex.setUnique(true);
0399: for (Field field : parentIndex) {
0400: mainIndex.add(builder.getField(field.getName()));
0401: }
0402: }
0403: }
0404:
0405: for (Element field : getChildElements("builder.fieldlist",
0406: "field")) {
0407: Element dbtype = getElementByPath(field, "field.db.type");
0408: if (dbtype != null) {
0409: String key = getElementAttributeValue(dbtype, "key");
0410: if (key != null && key.equalsIgnoreCase("true")) {
0411: String fieldName = getElementAttributeValue(field,
0412: "name");
0413: if ("".equals(fieldName)) {
0414: fieldName = getElementValue(getElementByPath(
0415: field, "field.db.name"));
0416: }
0417: if (mainIndex == null)
0418: mainIndex = new Index(builder, Index.MAIN);
0419: mainIndex.add(builder.getField(fieldName));
0420: }
0421: }
0422: }
0423: if (mainIndex != null) {
0424: results.add(mainIndex);
0425: }
0426:
0427: if (parentBuilder != null) {
0428: Collection<Index> parentIndices = parentBuilder
0429: .getStorageConnector().getIndices().values();
0430: if (parentIndices != null) {
0431: for (Index parentIndex : parentIndices) {
0432: Index newIndex = new Index(builder, parentIndex
0433: .getName());
0434: ;
0435: newIndex.setUnique(parentIndex.isUnique());
0436: for (Field field : parentIndex) {
0437: newIndex.add(builder.getField(field.getName()));
0438: }
0439: results.add(newIndex);
0440: }
0441: }
0442: }
0443:
0444: for (Element indexList : getChildElements("builder",
0445: "indexlist")) {
0446: for (Element indexElement : getChildElements(indexList,
0447: "index")) {
0448: String indexName = indexElement.getAttribute("name");
0449: if (indexName != null && !indexName.equals("")) {
0450: String unique = indexElement.getAttribute("unique");
0451: Index index = new Index(builder, indexName);
0452: index.setUnique(unique != null
0453: && unique.equals("true"));
0454: for (Element fieldElement : getChildElements(
0455: indexElement, "indexfield")) {
0456: String fieldName = fieldElement
0457: .getAttribute("name");
0458: Field field = builder.getField(fieldName);
0459: if (field == null) {
0460: log.error("field '" + fieldName
0461: + "' in index '" + indexName
0462: + "' in builder "
0463: + builder.getTableName()
0464: + " does not exist");
0465: } else {
0466: index.add(field);
0467: }
0468: }
0469: results.add(index);
0470: } else {
0471: log.error("index in builder "
0472: + builder.getTableName() + " has no name");
0473: }
0474: }
0475: }
0476: return results;
0477: }
0478:
0479: /**
0480: * @since MMBase-1.8
0481: */
0482: public Set<Function> getFunctions(final MMObjectBuilder builder) {
0483: Map<String, Function> results = new HashMap<String, Function>();
0484: for (Element functionList : getChildElements("builder",
0485: "functionlist")) {
0486: for (Element functionElement : getChildElements(
0487: functionList, "function")) {
0488: try {
0489: final String functionName = functionElement
0490: .getAttribute("name");
0491: String providerKey = functionElement
0492: .getAttribute("key");
0493: String functionClass = getNodeTextValue(getElementByPath(
0494: functionElement, "function.class"));
0495:
0496: Function function;
0497: log.debug("Using function class '" + functionClass
0498: + "'");
0499: final Class claz = Class.forName(functionClass);
0500: if (Function.class.isAssignableFrom(claz)) {
0501: if (!providerKey.equals("")) {
0502: log
0503: .warn("Specified a key attribute for a Function "
0504: + claz
0505: + " in "
0506: + getSystemId()
0507: + ", this makes only sense for FunctionProviders.");
0508: }
0509: function = (Function) claz.newInstance();
0510: } else if (FunctionProvider.class
0511: .isAssignableFrom(claz)) {
0512: if ("".equals(providerKey))
0513: providerKey = functionName;
0514: if ("".equals(providerKey)) {
0515: log.error("FunctionProvider " + claz
0516: + " specified in " + getSystemId()
0517: + " without key or name");
0518: continue;
0519: }
0520: FunctionProvider provider = (FunctionProvider) claz
0521: .newInstance();
0522: function = provider.getFunction(providerKey);
0523: if (function == null) {
0524: log.error("Function provider " + provider
0525: + "has no function '" + providerKey
0526: + "'");
0527: continue;
0528: }
0529: } else {
0530: if ("".equals(providerKey))
0531: providerKey = functionName;
0532: if ("".equals(providerKey)) {
0533: log
0534: .error("Speficied class "
0535: + claz
0536: + " in "
0537: + getSystemId()
0538: + "/functionslist/function is not a Function or FunctionProvider and can not be wrapped in a BeanFunction, because neither key nor name attribute were specified.");
0539: continue;
0540: }
0541: java.lang.reflect.Method method = MethodFunction
0542: .getMethod(claz, providerKey);
0543: if (method == null) {
0544: log.error("Could not find method '"
0545: + providerKey + "' in " + claz);
0546: continue;
0547: } else {
0548: if (method.getParameterTypes().length == 0) {
0549: function = BeanFunction.getFunction(
0550: claz, providerKey,
0551: new BeanFunction.Producer() {
0552: public Object getInstance() {
0553: try {
0554: return BeanFunction
0555: .getInstance(
0556: claz,
0557: builder);
0558: } catch (Exception e) {
0559: log
0560: .error(
0561: e
0562: .getMessage(),
0563: e);
0564: return null;
0565: }
0566: }
0567:
0568: public String toString() {
0569: return ""
0570: + claz
0571: .getName()
0572: + "."
0573: + builder
0574: .getTableName();
0575: }
0576: });
0577: } else {
0578: if (method.getClass().isInstance(
0579: builder)) {
0580: function = MethodFunction
0581: .getFunction(method,
0582: providerKey,
0583: builder);
0584: } else {
0585: function = MethodFunction
0586: .getFunction(method,
0587: providerKey);
0588: }
0589: }
0590: }
0591: }
0592: if (!functionName.equals("")
0593: && !function.getName().equals(functionName)) {
0594: log.debug("Wrapping " + function.getName()
0595: + " to " + functionName);
0596: function = new WrappedFunction(function) {
0597: public String getName() {
0598: return functionName;
0599: }
0600: };
0601: }
0602:
0603: String key = function.getName();
0604: Function existing = results.get(key);
0605:
0606: if (existing != null) {
0607: log.info("Function " + key
0608: + " already defined, will combine it");
0609: CombinedFunction cf;
0610: if (existing instanceof CombinedFunction) {
0611: cf = (CombinedFunction) existing;
0612: } else {
0613: cf = new CombinedFunction(key);
0614: cf.addFunction(existing);
0615: }
0616: cf.addFunction(function);
0617: function = cf;
0618: }
0619:
0620: NodeFunction nf = NodeFunction.wrap(function);
0621: if (nf != null)
0622: function = nf;
0623:
0624: results.put(key, function);
0625: log.debug("functions are now: " + results);
0626: } catch (ClassNotFoundException cnfe) {
0627: log.warn(cnfe.getMessage());
0628: } catch (Throwable e) {
0629: log.error(e.getMessage(), e);
0630: }
0631: }
0632: }
0633: Set<Function> r = new HashSet<Function>();
0634: for (Function fun : results.values()) {
0635: r.add(fun);
0636: }
0637: log.debug("Found functions " + r);
0638: return r;
0639:
0640: }
0641:
0642: /**
0643: * Determine an integer value from an elements body.
0644: * Used for the List, Search, and Edit position values.
0645: * @param elm The element containing the value.
0646: * @return the parsed integer
0647: */
0648: private int getEditorPos(Element elm) {
0649: try {
0650: int val = Integer.parseInt(getElementValue(elm));
0651: return val;
0652: } catch (Exception e) {
0653: return -1;
0654: }
0655: }
0656:
0657: /**
0658: * Alter a specified, named FieldDef object using information obtained from the buidler configuration.
0659: * Only GUI information is retrieved and stored (name and type of the field sg=hould already be specified).
0660: * @since MMBase-1.6
0661: * @param elm The element containing the field information acc. to the buidler xml format
0662: * @param def The field definition to alter
0663: */
0664: private void decodeFieldDef(Element field, CoreField def,
0665: DataTypeCollector collector) {
0666: // Gui
0667: Element descriptions = getElementByPath(field,
0668: "field.descriptions");
0669: if (descriptions != null) {
0670: def.getLocalizedDescription().fillFromXml("description",
0671: descriptions);
0672: }
0673:
0674: // XXX: deprecated tag 'gui'
0675: Element gui = getElementByPath(field, "field.gui");
0676: if (gui != null) {
0677: def.getLocalizedGUIName().fillFromXml("guiname", gui);
0678: // XXX: even more deprecated
0679: def.getLocalizedGUIName().fillFromXml("name", gui);
0680: }
0681:
0682: // Editor
0683: Element editorpos = getElementByPath(field,
0684: "field.editor.positions.input");
0685: if (editorpos != null) {
0686: int inputPos = getEditorPos(editorpos);
0687: if (inputPos > -1)
0688: inputPositions.add(inputPos);
0689: def.setEditPosition(inputPos);
0690: } else {
0691: // if not specified, use lowest 'free' position.
0692: int i = 1;
0693: while (inputPositions.contains(i)) {
0694: ++i;
0695: }
0696: inputPositions.add(i);
0697: def.setEditPosition(i);
0698:
0699: }
0700: editorpos = getElementByPath(field,
0701: "field.editor.positions.list");
0702: if (editorpos != null) {
0703: def.setListPosition(getEditorPos(editorpos));
0704: }
0705: editorpos = getElementByPath(field,
0706: "field.editor.positions.search");
0707: if (editorpos != null) {
0708: int searchPos = getEditorPos(editorpos);
0709: if (searchPos > -1)
0710: searchPositions.add(searchPos);
0711: def.setSearchPosition(searchPos);
0712: } else {
0713: // if not specified, use lowest 'free' position, unless, db-type is BINARY (non-sensical searching on that)
0714: // or the field is not in storage at all (search cannot be performed by database)
0715: if (def.getType() != Field.TYPE_BINARY && !def.isVirtual()) {
0716: int i = 1;
0717: while (searchPositions.contains(i)) {
0718: ++i;
0719: }
0720: searchPositions.add(i);
0721: def.setSearchPosition(i);
0722: } else {
0723: def.setSearchPosition(-1);
0724: }
0725: }
0726: }
0727:
0728: /**
0729: * Determine a data type instance based on the given gui element
0730: * @todo 'guitype' may become deprecated in favour of the 'datatype' element
0731: * @param builder the MMObjectBuilder to which the field belongs
0732: * @param collector The DataTypeCollector of the bulider.
0733: * @param fieldName the name of the field (used in log messages)
0734: * @param field The 'field' element of the builder xml
0735: * @param type The database type of the field
0736: * @param listItemType If the database type is a List, there is also a type of its element
0737: * @param forceInstance If true, it will never return <code>null</code>, but will return (a clone) of the DataType associated with the database type.
0738: * @since MMBase-1.8
0739: */
0740: protected DataType decodeDataType(final MMObjectBuilder builder,
0741: final DataTypeCollector collector, final String fieldName,
0742: final Element field, final int type,
0743: final int listItemType, final boolean forceInstance) {
0744: BasicDataType baseDataType = null;
0745: if (type == Field.TYPE_LIST) {
0746: baseDataType = DataTypes.getListDataType(listItemType);
0747: } else if (type != Field.TYPE_UNKNOWN) {
0748: baseDataType = DataTypes.getDataType(type);
0749: }
0750: BasicDataType dataType = null;
0751: Element guiTypeElement = getElementByPath(field,
0752: "field.gui.guitype");
0753:
0754: // XXX: deprecated tag 'type'
0755: if (guiTypeElement == null) {
0756: guiTypeElement = getElementByPath(field, "field.gui.type");
0757: }
0758:
0759: // Backwards compatible 'guitype' support
0760: if (guiTypeElement != null && collector != null) {
0761: if (baseDataType == null) {
0762: throw new IllegalArgumentException("No type defined");
0763: }
0764: String guiType = getElementValue(guiTypeElement);
0765: if (!guiType.equals("")) {
0766: if (guiType.indexOf('.') != -1) {
0767: // apparently, this is a class path, which means it is probably an enumeration
0768: // (if not, what else?)
0769: dataType = (BasicDataType) baseDataType.clone();
0770: dataType.getEnumerationFactory().addBundle(guiType,
0771: getClass().getClassLoader(), null,
0772: dataType.getTypeAsClass(), null);
0773: dataType.getEnumerationRestriction()
0774: .setEnforceStrength(DataType.ENFORCE_NEVER);
0775: } else {
0776: // check for builder names when the type is NODE
0777: MMObjectBuilder enumerationBuilder = null;
0778: // The guitype is deprecated. Normally coincides with datatype's id.
0779: // The following are exceptions:
0780: // 'string' is surrogated with the datatype 'line'.
0781: if ("string".equals(guiType)) {
0782: guiType = "line";
0783: if (log.isDebugEnabled()) {
0784: log
0785: .debug("Converted deprecated guitype 'string' for field "
0786: + (builder != null ? builder
0787: .getTableName()
0788: + "."
0789: : "")
0790: + fieldName
0791: + " with datatype 'line'.");
0792: }
0793: } else
0794: // 'eventtime' is surrogated with the datatype 'datetime'.
0795: if ("eventtime".equals(guiType)) {
0796: guiType = "datetime";
0797: if (log.isDebugEnabled()) {
0798: log
0799: .debug("Converted deprecated guitype 'eventtime' for field "
0800: + (builder != null ? builder
0801: .getTableName()
0802: + "."
0803: : "")
0804: + fieldName
0805: + " with datatype 'datetime'.");
0806: }
0807: } else
0808: // 'relativetime' is surrogated with the datatype 'line'.
0809: if ("relativetime".equals(guiType)) {
0810: guiType = "duration";
0811: if (log.isDebugEnabled()) {
0812: log
0813: .debug("Converted deprecated guitype 'relativetime' for field "
0814: + (builder != null ? builder
0815: .getTableName()
0816: + "."
0817: : "")
0818: + fieldName
0819: + " with datatype 'duration'.");
0820: }
0821: } else if (type == Field.TYPE_NODE) {
0822: if (guiType == null) {
0823: if (log.isDebugEnabled())
0824: log.debug("Gui type of NODE field '"
0825: + fieldName + "' is null");
0826: } else {
0827: enumerationBuilder = mmbase
0828: .getBuilder(guiType);
0829: if (enumerationBuilder == null) {
0830: if (log.isDebugEnabled())
0831: log
0832: .debug("Gui type of NODE field is '"
0833: + fieldName
0834: + "'not a known builder");
0835: }
0836: }
0837: }
0838: if (enumerationBuilder != null) {
0839: // Create a query element of the format:
0840: // <query type="[buildername]" xmlns="http://www.mmbase.org/xmlns/searchquery" />
0841: // and add it to the enumerationfactory using addQuery()
0842: Element queryElement = guiTypeElement
0843: .getOwnerDocument()
0844: .createElementNS(
0845: "http://www.mmbase.org/xmlns/searchquery",
0846: "query");
0847: queryElement.setAttribute("type",
0848: enumerationBuilder.getTableName());
0849: dataType = (BasicDataType) baseDataType.clone();
0850: Document queryDocument = DocumentReader
0851: .toDocument(queryElement);
0852: dataType
0853: .getEnumerationFactory()
0854: .addQuery(
0855: LocalizedString
0856: .getLocale(queryElement),
0857: queryDocument);
0858: dataType.getEnumerationRestriction()
0859: .setEnforceStrength(
0860: DataType.ENFORCE_NEVER);
0861: } else {
0862: dataType = collector.getDataTypeInstance(
0863: guiType, baseDataType);
0864: if (dataType == null) {
0865: log.warn("Could not find data type for "
0866: + baseDataType
0867: + " / "
0868: + guiType
0869: + " for builder: '"
0870: + (builder == null ? "NULL"
0871: : builder.getTableName())
0872: + "'");
0873:
0874: }
0875: }
0876: }
0877: }
0878: }
0879:
0880: Element dataTypeElement = getElementByPath(field,
0881: "field.datatype");
0882:
0883: if (dataTypeElement != null) {
0884: if (dataType != null) {
0885: log
0886: .warn("Using both deprecated 'gui/guitype' and 'datatype' subelements in field tag for field '"
0887: + fieldName
0888: + "', ignoring the first one.");
0889: }
0890: BasicDataType requestedBaseDataType; // pointer to the original field's datatype which will be used as a base.
0891: String base = dataTypeElement.getAttribute("base");
0892: if (base.equals("")) {
0893: if (log.isDebugEnabled()) {
0894: log.debug("No base defined, using '" + baseDataType
0895: + "'");
0896: }
0897: if (baseDataType == null) {
0898: throw new IllegalArgumentException(
0899: "No base datatype given, and no field type defined");
0900: }
0901: requestedBaseDataType = baseDataType;
0902: } else {
0903: requestedBaseDataType = collector == null ? null
0904: : collector.getDataType(base, true);
0905: if (requestedBaseDataType == null) {
0906: log.error("Could not find base datatype for '"
0907: + base
0908: + "' falling back to "
0909: + baseDataType
0910: + " in builder '"
0911: + (builder == null ? "NULL" : builder
0912: .getTableName()) + "'");
0913: requestedBaseDataType = baseDataType;
0914: }
0915: }
0916: try {
0917: dataType = DataTypeReader.readDataType(dataTypeElement,
0918: requestedBaseDataType, collector).dataType;
0919: } catch (DependencyException de) {
0920: dataType = de.fallback();
0921: }
0922: if (log.isDebugEnabled())
0923: log.debug("Found datatype " + dataType + " for field "
0924: + fieldName);
0925: }
0926:
0927: // try to resolve any issues where the datatype differs from the database type
0928: if (dataType != null
0929: && baseDataType != null
0930: && !baseDataType.getClass().isAssignableFrom(
0931: dataType.getClass())) {
0932: // the thus configured datatype is not compatible with the database type.
0933: // Fix that as good as possible:
0934: BasicDataType newDataType = (BasicDataType) dataType
0935: .clone();
0936: newDataType.inherit(baseDataType);
0937: if (log.isDebugEnabled())
0938: log
0939: .debug(""
0940: + dataType
0941: + " in '"
0942: + getSystemId()
0943: + "' field "
0944: + fieldName
0945: + " is not compatible with "
0946: + baseDataType
0947: + ". Cloning and inheriting to support gracefull fall backs -> "
0948: + newDataType);
0949: dataType = newDataType;
0950: }
0951:
0952: if (dataType == null && forceInstance) {
0953: // DataType is null if no data type element was found
0954: if (baseDataType == null) {
0955: throw new IllegalArgumentException(
0956: "No datatype given, and no type defined");
0957: }
0958: dataType = (BasicDataType) baseDataType.clone(""); // clone with empty id
0959: }
0960:
0961: return dataType;
0962: }
0963:
0964: /**
0965: * @since MMBase-1.8.6
0966: */
0967: private void decodeFieldAttributes(Element field, CoreField def) {
0968: String fieldState = getElementAttributeValue(field, "state");
0969: String fieldReadOnly = getElementAttributeValue(field,
0970: "readonly");
0971: // deprecated db type tag - only use if no other data is given!
0972: Element dbtype = getElementByPath(field, "field.db.type");
0973: if (dbtype != null) {
0974: if ("".equals(fieldState))
0975: fieldState = getElementAttributeValue(dbtype, "state");
0976: if ("".equals(fieldReadOnly))
0977: fieldReadOnly = getElementAttributeValue(dbtype,
0978: "readonly");
0979: }
0980:
0981: // state - default peristent
0982: int state = Field.STATE_PERSISTENT;
0983: if (!"".equals(fieldState)) {
0984: state = Fields.getState(fieldState);
0985: }
0986: if (state != def.getState())
0987: def.setState(state);
0988:
0989: boolean readOnly = false;
0990: if ("".equals(fieldReadOnly)) {
0991: readOnly = state == Field.STATE_SYSTEM
0992: || state == Field.STATE_SYSTEM_VIRTUAL;
0993: } else {
0994: readOnly = "true".equalsIgnoreCase(fieldReadOnly);
0995: }
0996:
0997: if (def.isReadOnly() != readOnly) {
0998: def.setReadOnly(readOnly);
0999: }
1000: }
1001:
1002: /**
1003: * Construct a FieldDef object using a field Element using information
1004: * obtained from the builder configuration.
1005: * @since MMBase-1.8
1006: */
1007: private CoreField decodeFieldDef(MMObjectBuilder builder,
1008: DataTypeCollector collector, Element field) {
1009: // create a new CoreField we need to fill
1010:
1011: // obtain field name.
1012: // if both the field name attribute and the <db><name> tag are specified, the attribute takes precedence.
1013: String fieldName = getElementAttributeValue(field, "name");
1014: String fieldDBName = getElementValue(getElementByPath(field,
1015: "field.db.name"));
1016: if ("".equals(fieldName)) {
1017: if ("".equals(fieldDBName)) {
1018: throw new IllegalArgumentException(
1019: "Field name was not specified for builder "
1020: + builder.getTableName() + ".");
1021: }
1022: if (log.isDebugEnabled()) {
1023: log.debug("<db><name> tag for field '" + fieldDBName
1024: + "' is deprecated. Use the name attribute.");
1025: }
1026: fieldName = fieldDBName;
1027: } else if (!"".equals(fieldDBName)) {
1028: log
1029: .warn("Specified field name twice: once in the name attribute ('"
1030: + fieldName
1031: + "') and once in the <name> tag ('"
1032: + fieldDBName + "'). Ignoring name tag.");
1033: }
1034:
1035: // implied by datatype
1036: // use db/type to override for legacy database issues
1037: // (mostly to prevent warnings in the log, as mmbase fixes this anyway)
1038: String fieldType = "";
1039: String fieldSize = "";
1040: String fieldNotNull = "";
1041:
1042: // defined in datatype
1043: String fieldRequired = "";
1044: String fieldUnique = "";
1045:
1046: // deprecated db type tag - only use if no other data is given!
1047: Element dbtype = getElementByPath(field, "field.db.type");
1048: if (dbtype != null) {
1049: if (!"".equals(fieldType) || !"".equals(fieldNotNull)
1050: || !"".equals(fieldSize)) {
1051: log
1052: .warn("Specified field type info for '"
1053: + fieldName
1054: + "' twice: once in the field tag attributes and once in the <db><type> tag.");
1055: } else {
1056: if (log.isDebugEnabled()) {
1057: log.debug("<db><type> tag for field '" + fieldName
1058: + "' is deprecated.");
1059: }
1060: fieldType = getElementValue(dbtype);
1061: fieldNotNull = getElementAttributeValue(dbtype,
1062: "notnull");
1063: fieldRequired = getElementAttributeValue(dbtype,
1064: "required");
1065: fieldUnique = getElementAttributeValue(dbtype, "unique");
1066: fieldSize = getElementAttributeValue(dbtype, "size");
1067: }
1068: }
1069:
1070: // type - default unknown (derived from datatype)
1071: int type = Field.TYPE_UNKNOWN;
1072: int listItemType = Field.TYPE_UNKNOWN;
1073: if (!"".equals(fieldType)) {
1074: type = Fields.getType(fieldType);
1075: if (type == Field.TYPE_LIST) {
1076: if (fieldType.length() > 5) {
1077: listItemType = Fields.getType(fieldType.substring(
1078: 5, fieldType.length() - 1));
1079: }
1080: }
1081: }
1082:
1083: // datatype
1084: DataType dataType = decodeDataType(builder, collector,
1085: fieldName, field, type, listItemType, true);
1086:
1087: // determine type from datatype, if possible)
1088: if (type == Field.TYPE_UNKNOWN) {
1089: type = dataType.getBaseType();
1090: if (type == Field.TYPE_LIST) {
1091: listItemType = ((ListDataType) dataType)
1092: .getItemDataType().getBaseType();
1093: }
1094: }
1095:
1096: CoreField def = Fields.createField(fieldName, type,
1097: listItemType, Field.STATE_VIRTUAL,/*temp default, will set by decodeFieldAttributes*/
1098: dataType);
1099: dataType = def.getDataType();
1100:
1101: decodeFieldAttributes(field, def);
1102:
1103: def.setParent(builder);
1104:
1105: if (!fieldSize.equals("")) {
1106: try {
1107: def.setMaxLength(Integer.parseInt(fieldSize));
1108: } catch (NumberFormatException e) {
1109: log.warn("invalid value for size : " + fieldSize);
1110: }
1111: }
1112:
1113: // set required property, but only if given
1114: if (!"".equals(fieldRequired)) {
1115: dataType
1116: .setRequired("true".equalsIgnoreCase(fieldRequired));
1117: }
1118:
1119: // default for notnull is value of required
1120: def.setNotNull("true".equals(fieldNotNull)
1121: || ("".equals(fieldNotNull) && dataType.isRequired()));
1122:
1123: // set unique property, but only if given
1124: if ("implied".equalsIgnoreCase(fieldUnique)) {
1125: dataType.setUnique(true);
1126: dataType.getUniqueRestriction().setEnforceStrength(
1127: DataType.ENFORCE_NEVER);
1128: } else if ("true".equalsIgnoreCase(fieldUnique)) {
1129: dataType.setUnique(true);
1130: }
1131:
1132: decodeFieldDef(field, def, collector);
1133:
1134: return def;
1135: }
1136:
1137: /**
1138: * Get the properties of this builder
1139: * @code-conventions return type should be Map
1140: * @return the properties in a Map (as name-value pairs)
1141: */
1142: public Hashtable<String, String> getProperties() {
1143: Hashtable<String, String> results = new Hashtable<String, String>();
1144: if (parentBuilder != null) {
1145: Map<String, String> parentparams = parentBuilder
1146: .getInitParameters();
1147: if (parentparams != null) {
1148: results.putAll(parentparams);
1149: }
1150: }
1151: for (Element p : getChildElements("builder.properties",
1152: "property")) {
1153: String name = getElementAttributeValue(p, "name");
1154: String value = getElementValue(p);
1155: results.put(name, value);
1156: }
1157: return results;
1158: }
1159:
1160: /**
1161: * Get the descriptions of this module.
1162: * @return the descriptions as a LocalizedString
1163: */
1164: public LocalizedString getLocalizedDescription(
1165: LocalizedString description) {
1166: description.fillFromXml("description",
1167: getElementByPath("builder.descriptions"));
1168: return description;
1169: }
1170:
1171: /**
1172: * Get the (gui) names of this module.
1173: * @return the names as a LocalizedString
1174: */
1175: public LocalizedString getLocalizedSingularName(
1176: LocalizedString guiName) {
1177: guiName.fillFromXml("singular",
1178: getElementByPath("builder.names"));
1179: return guiName;
1180: }
1181:
1182: /**
1183: * Get the (gui) names of this module.
1184: * @return the names as a LocalizedString
1185: */
1186: public LocalizedString getLocalizedPluralName(
1187: LocalizedString guiName) {
1188: guiName
1189: .fillFromXml("plural",
1190: getElementByPath("builder.names"));
1191: return guiName;
1192: }
1193:
1194: /**
1195: * Get the descriptions of this builder
1196: * @deprecated use getLocalizedDescription()
1197: * @return the descriptions in a Map, accessible by language
1198: */
1199: public Hashtable<String, String> getDescriptions() {
1200: Hashtable<String, String> results = new Hashtable<String, String>();
1201: for (Element desc : getChildElements("builder.descriptions",
1202: "description")) {
1203: String lang = getElementAttributeValue(desc, "xml:lang");
1204: results.put(lang, getElementValue(desc));
1205: }
1206: return results;
1207: }
1208:
1209: /**
1210: * Get the plural names of this builder
1211: * @deprecated use getLocalizedPluralName()
1212: * @return the plural names in a Map, accessible by language
1213: */
1214: public Hashtable<String, String> getPluralNames() {
1215: Hashtable<String, String> results = new Hashtable<String, String>();
1216: for (Element name : getChildElements("builder.names", "plural")) {
1217: String lang = getElementAttributeValue(name, "xml:lang");
1218: results.put(lang, getElementValue(name));
1219: }
1220: return results;
1221: }
1222:
1223: /**
1224: * Get the singular (GUI) names of this builder
1225: * @deprecated use getLocalizedSingularName()
1226: * @return the singular names in a Map, accessible by language
1227: */
1228: public Hashtable<String, String> getSingularNames() {
1229: Hashtable<String, String> results = new Hashtable<String, String>();
1230: for (Element name : getChildElements("builder.names",
1231: "singular")) {
1232: String lang = getElementAttributeValue(name, "xml:lang");
1233: results.put(lang, getElementValue(name));
1234: }
1235: return results;
1236: }
1237:
1238: /**
1239: * Get the builder that this builder extends
1240: *
1241: * @since MMBase-1.6
1242: * @return the parent as an MMObjectBuilder, or null if not specified or unresolved
1243: */
1244: public MMObjectBuilder getParentBuilder() {
1245: return parentBuilder;
1246: }
1247:
1248: /**
1249: * Get the name of the builder that this builder extends
1250: * @since MMBase-1.8
1251: * @return the name of the parent builder
1252: */
1253: public String getExtends() {
1254: return getElementAttributeValue("builder", "extends");
1255: }
1256:
1257: /**
1258: * Retrieve the (major) version number of this builder
1259: * @since MMBase-1.8
1260: * @return the version as an integer.
1261: */
1262: public int getVersion() {
1263: String version = getElementAttributeValue("builder", "version");
1264: if (version.equals("") && parentBuilder != null) {
1265: return parentBuilder.getVersion();
1266: } else {
1267: int n = 0;
1268: if (!version.equals("")) {
1269: try {
1270: n = Integer.parseInt(version);
1271: } catch (Exception f) {
1272: }
1273: }
1274: return n;
1275: }
1276: }
1277:
1278: /**
1279: * Retrieve the name of the maintainer of this builder
1280: * @since MMBase-1.8
1281: * @return the name fo the maintainer as a String
1282: */
1283: public String getMaintainer() {
1284: String maintainer = getElementAttributeValue("builder",
1285: "maintainer");
1286: if (maintainer.equals("")) {
1287: if (parentBuilder != null) {
1288: maintainer = parentBuilder.getMaintainer();
1289: } else {
1290: maintainer = "mmbase.org";
1291: }
1292: }
1293: return maintainer;
1294: }
1295:
1296: /**
1297: * {@inheritDoc}
1298: * @since MMBase-1.7
1299: */
1300: public boolean equals(Object o) {
1301: if (o instanceof BuilderReader) {
1302: BuilderReader b = (BuilderReader) o;
1303: List<CoreField> fields = getFields();
1304: List<CoreField> otherFields = b.getFields();
1305: return fields.equals(otherFields)
1306: && getMaintainer().equals(b.getMaintainer())
1307: && getVersion() == b.getVersion()
1308: && getExtends().equals(b.getExtends())
1309: && getSingularNames().equals(b.getSingularNames())
1310: && getPluralNames().equals(b.getPluralNames())
1311: && getDescriptions().equals(b.getDescriptions())
1312: && getProperties().equals(b.getProperties())
1313: && getClassName().equals(b.getClassName());
1314: } else {
1315: return false;
1316: }
1317: }
1318:
1319: /**
1320: * Whether this builderreader object is equal to another for storage purposes (so, ignoring gui and documentation fields)
1321: * @since MMBase-1.7
1322: */
1323: public boolean storageEquals(BuilderReader f) {
1324: List<CoreField> otherFields = f.getFields();
1325: List<CoreField> this Fields = getFields();
1326: if (otherFields.size() != this Fields.size())
1327: return false;
1328: for (int i = 0; i < this Fields.size(); i++) {
1329: CoreField this Field = this Fields.get(i);
1330: CoreField otherField = otherFields.get(i);
1331: if (!this Field.storageEquals(otherField))
1332: return false;
1333: }
1334: return true;
1335: }
1336:
1337: /**
1338: * For testing only
1339: */
1340: public static void main(String[] argv) throws Exception {
1341: org.mmbase.util.ResourceLoader rl = org.mmbase.util.ResourceLoader
1342: .getSystemRoot();
1343: Document doc = rl.getDocument(argv[0], true,
1344: BuilderReader.class);
1345: new BuilderReader(doc, null);
1346: }
1347:
1348: }
|