001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 * 
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.file;
029
030import org.opencms.main.CmsRuntimeException;
031import org.opencms.util.CmsStringUtil;
032
033import java.io.Serializable;
034import java.util.ArrayList;
035import java.util.Collections;
036import java.util.HashMap;
037import java.util.Iterator;
038import java.util.List;
039import java.util.Map;
040import java.util.RandomAccess;
041
042/**
043 * Represents a property (meta-information) mapped to a VFS resource.<p>
044 * 
045 * A property is an object that contains three string values: a name, a property value which is mapped
046 * to the structure record of a resource, and a property value which is mapped to the resource
047 * record of a resource. A property object is valid if it has both values or just one value set.
048 * Each property needs at least a name and one value set.<p>
049 * 
050 * A property value mapped to the structure record of a resource is significant for a single
051 * resource (sibling). A property value mapped to the resource record of a resource is significant
052 * for all siblings of a resource record. This is possible by getting the "compound value" 
053 * (see {@link #getValue()}) of a property in case a property object has both values set. The compound 
054 * value of a property object is the value mapped to the structure record, because it's structure 
055 * value is more significant than it's resource value. This allows to set a property only one time 
056 * on the resource record, and the property takes effect on all siblings of this resource record.<p>
057 * 
058 * The ID of the structure or resource record where a property value is mapped to is represented by 
059 * the "PROPERTY_MAPPING_ID" table attribute in the database. The "PROPERTY_MAPPING_TYPE" table 
060 * attribute (see {@link #STRUCTURE_RECORD_MAPPING} and {@link #RESOURCE_RECORD_MAPPING})
061 * determines whether the value of the "PROPERTY_MAPPING_ID" attribute of the current row is
062 * a structure or resource record ID.<p>
063 * 
064 * Property objects are written to the database using {@link org.opencms.file.CmsObject#writePropertyObject(String, CmsProperty)}
065 * or {@link org.opencms.file.CmsObject#writePropertyObjects(String, List)}, no matter
066 * whether you want to save a new (non-existing) property, update an existing property, or delete an
067 * existing property. To delete a property you would write a property object with either the
068 * structure and/or resource record values set to {@link #DELETE_VALUE} to indicate that a
069 * property value should be deleted in the database. Set property values to null if they should
070 * remain unchanged in the database when a property object is written. As for example you want to
071 * update just the structure value of a property, you would set the structure value to the new string,
072 * and the resource value to null (which is already the case by default).<p>
073 * 
074 * Use {@link #setAutoCreatePropertyDefinition(boolean)} to set a boolean flag whether a missing property
075 * definition should be created implicitly for a resource type when a property is written to the database.
076 * The default value for this flag is <code>false</code>. Thus, you receive a CmsException if you try
077 * to write a property of a resource with a resource type which lacks a property definition for
078 * this resource type. It is not a good style to set {@link #setAutoCreatePropertyDefinition(boolean)}
079 * on true to make writing properties to the database work in any case, because then you will loose
080 * control about which resource types support which property definitions.<p>
081 * 
082 * @since 6.0.0 
083 */
084public class CmsProperty implements Serializable, Cloneable, Comparable<CmsProperty> {
085
086    /**
087     * Signals that the resource property values of a resource
088     * should be deleted using deleteAllProperties.<p>
089     */
090    public static final int DELETE_OPTION_DELETE_RESOURCE_VALUES = 3;
091
092    /**
093     * Signals that both the structure and resource property values of a resource
094     * should be deleted using deleteAllProperties.<p>
095     */
096    public static final int DELETE_OPTION_DELETE_STRUCTURE_AND_RESOURCE_VALUES = 1;
097
098    /**
099     * Signals that the structure property values of a resource
100     * should be deleted using deleteAllProperties.<p>
101     */
102    public static final int DELETE_OPTION_DELETE_STRUCTURE_VALUES = 2;
103
104    /**
105     * An empty string to decide that a property value should be deleted when this
106     * property object is written to the database.<p>
107     */
108    public static final String DELETE_VALUE = "";
109
110    /**
111     * Value of the "mapping-type" database attribute to indicate that a property value is mapped
112     * to a resource record.<p>
113     */
114    public static final int RESOURCE_RECORD_MAPPING = 2;
115
116    /**
117     * Value of the "mapping-type" database attribute to indicate that a property value is mapped
118     * to a structure record.<p>
119     */
120    public static final int STRUCTURE_RECORD_MAPPING = 1;
121
122    /** Key used for a individual (structure) property value. */
123    public static final String TYPE_INDIVIDUAL = "individual";
124
125    /** Key used for a shared (resource) property value. */
126    public static final String TYPE_SHARED = "shared";
127
128    /** The delimiter value for separating values in a list, per default this is the <code>|</code> char. */
129    public static final char VALUE_LIST_DELIMITER = '|';
130
131    /** The list delimiter replacement String used if the delimiter itself is contained in a String value. */
132    public static final String VALUE_LIST_DELIMITER_REPLACEMENT = "%(ld)";
133
134    /** The delimiter value for separating values in a map, per default this is the <code>=</code> char. */
135    public static final char VALUE_MAP_DELIMITER = '=';
136
137    /** The map delimiter replacement String used if the delimiter itself is contained in a String value. */
138    public static final String VALUE_MAP_DELIMITER_REPLACEMENT = "%(md)";
139
140    /** The null property object to be used in caches if a property is not found. */
141    private static final CmsProperty NULL_PROPERTY = new CmsProperty();
142
143    /** Serial version UID required for safe serialization. */
144    private static final long serialVersionUID = 93613508924212782L;
145
146    /**
147     * Boolean flag to decide if the property definition for this property should be created 
148     * implicitly on any write operation if doesn't exist already.<p>
149     */
150    private boolean m_autoCreatePropertyDefinition;
151
152    /** Indicates if the property is frozen (required for <code>NULL_PROPERTY</code>). */
153    private boolean m_frozen;
154
155    /** The name of this property. */
156    private String m_name;
157
158    /** The origin root path of the property. */
159    private String m_origin;
160
161    /** The value of this property attached to the resource record. */
162    private String m_resourceValue;
163
164    /** The (optional) value list of this property attached to the resource record. */
165    private List<String> m_resourceValueList;
166
167    /** The (optional) value map of this property attached to the resource record. */
168    private Map<String, String> m_resourceValueMap;
169
170    /** The value of this property attached to the structure record. */
171    private String m_structureValue;
172
173    /** The (optional) value list of this property attached to the structure record. */
174    private List<String> m_structureValueList;
175
176    /** The (optional) value map of this property attached to the structure record. */
177    private Map<String, String> m_structureValueMap;
178
179    /**
180     * Creates a new CmsProperty object.<p>
181     * 
182     * The structure and resource property values are initialized to null. The structure and
183     * resource IDs are initialized to {@link org.opencms.util.CmsUUID#getNullUUID()}.<p>
184     */
185    public CmsProperty() {
186
187        // nothing to do, all values will be initialized with <code>null</code> or <code>false</code> by default
188    }
189
190    /**
191     * Creates a new CmsProperty object using the provided values.<p>
192     *
193     * If the property definition does not exist for the resource type it
194     * is automatically created when this property is written.
195     * 
196     * @param name the name of the property definition
197     * @param structureValue the value to write as structure property
198     * @param resourceValue the value to write as resource property 
199     */
200    public CmsProperty(String name, String structureValue, String resourceValue) {
201
202        this(name, structureValue, resourceValue, true);
203    }
204
205    /**
206     * Creates a new CmsProperty object using the provided values.<p>
207     * 
208     * If <code>null</code> is supplied for the resource or structure value, this 
209     * value will not be available for this property.<p>
210     * 
211     * @param name the name of the property definition
212     * @param structureValue the value to write as structure property, or <code>null</code>
213     * @param resourceValue the value to write as resource property , or <code>null</code>
214     * @param autoCreatePropertyDefinition if <code>true</code>, the property definition for this property will be 
215     *      created implicitly on any write operation if it doesn't exist already
216     */
217    public CmsProperty(String name, String structureValue, String resourceValue, boolean autoCreatePropertyDefinition) {
218
219        m_name = name.trim();
220        m_structureValue = structureValue;
221        m_resourceValue = resourceValue;
222        m_autoCreatePropertyDefinition = autoCreatePropertyDefinition;
223    }
224
225    /**
226     * Static initializer required for freezing the <code>{@link #NULL_PROPERTY}</code>.<p>
227     */
228    static {
229
230        NULL_PROPERTY.m_frozen = true;
231        NULL_PROPERTY.m_name = "";
232    }
233
234    /**
235     * Searches in a list for the first occurrence of a {@link CmsProperty} object with the given name.<p> 
236     *
237     * To check if the "null property" has been returned if a property was 
238     * not found, use {@link #isNullProperty()} on the result.<p> 
239     *
240     * @param name a property name
241     * @param list a list of {@link CmsProperty} objects
242     * @return the index of the first occurrence of the name in they specified list, 
243     *      or {@link CmsProperty#getNullProperty()} if the name is not found
244     */
245    public static final CmsProperty get(String name, List<CmsProperty> list) {
246
247        CmsProperty property = null;
248        name = name.trim();
249        // choose the fastest method to traverse the list
250        if (list instanceof RandomAccess) {
251            for (int i = 0, n = list.size(); i < n; i++) {
252                property = list.get(i);
253                if (property.m_name.equals(name)) {
254                    return property;
255                }
256            }
257        } else {
258            Iterator<CmsProperty> i = list.iterator();
259            while (i.hasNext()) {
260                property = i.next();
261                if (property.m_name.equals(name)) {
262                    return property;
263                }
264            }
265        }
266
267        return NULL_PROPERTY;
268    }
269
270    /**
271     * Returns the null property object.<p>
272     * 
273     * @return the null property object
274     */
275    public static final CmsProperty getNullProperty() {
276
277        return NULL_PROPERTY;
278    }
279
280    /**
281     * Calls <code>{@link #setAutoCreatePropertyDefinition(boolean)}</code> for each
282     * property object in the given List with the given <code>value</code> parameter.<p>
283     * 
284     * This method will modify the objects in the input list directly.<p>
285     * 
286     * @param list a list of {@link CmsProperty} objects to modify
287     * @param value boolean value
288     * 
289     * @return the modified list of {@link CmsProperty} objects
290     * 
291     * @see #setAutoCreatePropertyDefinition(boolean)
292     */
293    public static final List<CmsProperty> setAutoCreatePropertyDefinitions(List<CmsProperty> list, boolean value) {
294
295        CmsProperty property;
296
297        // choose the fastest method to traverse the list
298        if (list instanceof RandomAccess) {
299            for (int i = 0, n = list.size(); i < n; i++) {
300                property = list.get(i);
301                property.m_autoCreatePropertyDefinition = value;
302            }
303        } else {
304            Iterator<CmsProperty> i = list.iterator();
305            while (i.hasNext()) {
306                property = i.next();
307                property.m_autoCreatePropertyDefinition = value;
308            }
309        }
310
311        return list;
312    }
313
314    /**
315     * Calls <code>{@link #setFrozen(boolean)}</code> for each
316     * {@link CmsProperty} object in the given List if it is not already frozen.<p>
317     * 
318     * This method will modify the objects in the input list directly.<p>
319     * 
320     * @param list a list of {@link CmsProperty} objects
321     * 
322     * @return the modified list of properties
323     * 
324     * @see #setFrozen(boolean)
325     */
326    public static final List<CmsProperty> setFrozen(List<CmsProperty> list) {
327
328        CmsProperty property;
329
330        // choose the fastest method to traverse the list
331        if (list instanceof RandomAccess) {
332            for (int i = 0, n = list.size(); i < n; i++) {
333                property = list.get(i);
334                if (!property.isFrozen()) {
335                    property.setFrozen(true);
336                }
337            }
338        } else {
339            Iterator<CmsProperty> i = list.iterator();
340            while (i.hasNext()) {
341                property = i.next();
342                if (!property.isFrozen()) {
343                    property.setFrozen(true);
344                }
345            }
346        }
347
348        return list;
349    }
350
351    /**
352     * Transforms a Map of String values into a list of 
353     * {@link CmsProperty} objects with the property name set from the
354     * Map key, and the structure value set from the Map value.<p>
355     * 
356     * @param map a Map with String keys and String values
357     * 
358     * @return a list of {@link CmsProperty} objects
359     */
360    public static List<CmsProperty> toList(Map<String, String> map) {
361
362        if ((map == null) || (map.size() == 0)) {
363            return Collections.emptyList();
364        }
365
366        List<CmsProperty> result = new ArrayList<CmsProperty>(map.size());
367        Iterator<Map.Entry<String, String>> i = map.entrySet().iterator();
368        while (i.hasNext()) {
369            Map.Entry<String, String> e = i.next();
370            CmsProperty property = new CmsProperty(e.getKey(), e.getValue(), null);
371            result.add(property);
372        }
373
374        return result;
375    }
376
377    /**
378     * Transforms a list of {@link CmsProperty} objects into a Map which uses the property name as
379     * Map key (String), and the property value as Map value (String).<p>
380     * 
381     * @param list a list of {@link CmsProperty} objects
382     * 
383     * @return a Map which uses the property names as
384     *      Map keys (String), and the property values as Map values (String)
385     */
386    public static Map<String, String> toMap(List<CmsProperty> list) {
387
388        if ((list == null) || (list.size() == 0)) {
389            return Collections.emptyMap();
390        }
391
392        String name = null;
393        String value = null;
394        CmsProperty property = null;
395        Map<String, String> result = new HashMap<String, String>(list.size());
396
397        // choose the fastest method to traverse the list
398        if (list instanceof RandomAccess) {
399            for (int i = 0, n = list.size(); i < n; i++) {
400                property = list.get(i);
401                name = property.m_name;
402                value = property.getValue();
403                result.put(name, value);
404            }
405        } else {
406            Iterator<CmsProperty> i = list.iterator();
407            while (i.hasNext()) {
408                property = i.next();
409                name = property.m_name;
410                value = property.getValue();
411                result.put(name, value);
412            }
413        }
414
415        return result;
416    }
417
418    /**
419     * Stores a collection of properties in a map, with the property names as keys.<p>
420     * 
421     * @param properties the properties to store in the map 
422     * 
423     * @return the map with the property names as keys and the property objects as values 
424     */
425    public static Map<String, CmsProperty> toObjectMap(Iterable<CmsProperty> properties) {
426
427        Map<String, CmsProperty> result = new HashMap<String, CmsProperty>();
428        for (CmsProperty property : properties) {
429            result.put(property.getName(), property);
430        }
431        return result;
432    }
433
434    /**
435     * Checks if the property definition for this property will be 
436     * created implicitly on any write operation if doesn't already exist.<p>
437     * 
438     * @return <code>true</code>, if the property definition for this property will be created implicitly on any write operation
439     */
440    public boolean autoCreatePropertyDefinition() {
441
442        return m_autoCreatePropertyDefinition;
443    }
444
445    /**
446     * Creates a clone of this property.<p>
447     *  
448     * @return a clone of this property
449     * 
450     * @see #cloneAsProperty()
451     */
452    @Override
453    public CmsProperty clone() {
454
455        return cloneAsProperty();
456    }
457
458    /**
459     * Creates a clone of this property that already is of type <code>{@link CmsProperty}</code>.<p>
460     * 
461     * The cloned property will not be frozen.<p>
462     * 
463     * @return a clone of this property that already is of type <code>{@link CmsProperty}</code>
464     */
465    public CmsProperty cloneAsProperty() {
466
467        if (this == NULL_PROPERTY) {
468            // null property must never be cloned
469            return NULL_PROPERTY;
470        }
471        CmsProperty clone = new CmsProperty();
472        clone.m_name = m_name;
473        clone.m_structureValue = m_structureValue;
474        clone.m_structureValueList = m_structureValueList;
475        clone.m_resourceValue = m_resourceValue;
476        clone.m_resourceValueList = m_resourceValueList;
477        clone.m_autoCreatePropertyDefinition = m_autoCreatePropertyDefinition;
478        clone.m_origin = m_origin;
479        // the value for m_frozen does not need to be set as it is false by default
480
481        return clone;
482    }
483
484    /**
485     * Compares this property to another Object.<p>
486     * 
487     * @param obj the other object to be compared
488     * @return if the argument is a property object, returns zero if the name of the argument is equal to the name of this property object, 
489     *      a value less than zero if the name of this property is lexicographically less than the name of the argument, 
490     *      or a value greater than zero if the name of this property is lexicographically greater than the name of the argument 
491     */
492    public int compareTo(CmsProperty obj) {
493
494        if (obj == this) {
495            return 0;
496        }
497        return m_name.compareTo(obj.m_name);
498    }
499
500    /**
501     * Tests if a specified object is equal to this CmsProperty object.<p>
502     * 
503     * Two property objects are equal if their names are equal.<p>
504     * 
505     * In case you want to compare the values as well as the name, 
506     * use {@link #isIdentical(CmsProperty)} instead.<p>
507     * 
508     * @param obj another object
509     * @return true, if the specified object is equal to this CmsProperty object
510     * 
511     * @see #isIdentical(CmsProperty)
512     */
513    @Override
514    public boolean equals(Object obj) {
515
516        if (obj == this) {
517            return true;
518        }
519        if (obj instanceof CmsProperty) {
520            return ((CmsProperty)obj).m_name.equals(m_name);
521        }
522        return false;
523    }
524
525    /**
526     * Returns the name of this property.<p>
527     * 
528     * @return the name of this property
529     */
530    public String getName() {
531
532        return m_name;
533    }
534
535    /**
536     * Returns the root path of the resource from which the property was read.<p>
537     * 
538     * @return the root path of the resource from which the property was read
539     */
540    public String getOrigin() {
541
542        return m_origin;
543    }
544
545    /**
546     * Returns the value of this property attached to the resource record.<p>
547     * 
548     * @return the value of this property attached to the resource record
549     */
550    public String getResourceValue() {
551
552        return m_resourceValue;
553    }
554
555    /**
556     * Returns the value of this property attached to the resource record, split as a list.<p>
557     * 
558     * This list is build form the resource value, which is split into separate values
559     * using the <code>|</code> char as delimiter. If the delimiter is not found,
560     * then the list will contain one entry which is equal to <code>{@link #getResourceValue()}</code>.<p>
561     * 
562     * @return the value of this property attached to the resource record, split as a (unmodifiable) list of Strings
563     */
564    public List<String> getResourceValueList() {
565
566        if ((m_resourceValueList == null) && (m_resourceValue != null)) {
567            // use lazy initializing of the list
568            m_resourceValueList = createListFromValue(m_resourceValue);
569            m_resourceValueList = Collections.unmodifiableList(m_resourceValueList);
570        }
571        return m_resourceValueList;
572    }
573
574    /**
575     * Returns the value of this property attached to the resource record as a map.<p>
576     * 
577     * This map is build from the used value, which is split into separate key/value pairs
578     * using the <code>|</code> char as delimiter. If the delimiter is not found,
579     * then the map will contain one entry.<p>
580     * 
581     * The key/value pairs are separated with the <code>=</code>.<p>
582     * 
583     * @return the value of this property attached to the resource record, as an (unmodifiable) map of Strings
584     */
585    public Map<String, String> getResourceValueMap() {
586
587        if ((m_resourceValueMap == null) && (m_resourceValue != null)) {
588            // use lazy initializing of the map
589            m_resourceValueMap = createMapFromValue(m_resourceValue);
590            m_resourceValueMap = Collections.unmodifiableMap(m_resourceValueMap);
591        }
592        return m_resourceValueMap;
593    }
594
595    /**
596     * Returns the value of this property attached to the structure record.<p>
597     * 
598     * @return the value of this property attached to the structure record
599     */
600    public String getStructureValue() {
601
602        return m_structureValue;
603    }
604
605    /**
606     * Returns the value of this property attached to the structure record, split as a list.<p>
607     * 
608     * This list is build form the structure value, which is split into separate values
609     * using the <code>|</code> char as delimiter. If the delimiter is not found,
610     * then the list will contain one entry which is equal to <code>{@link #getStructureValue()}</code>.<p>
611     * 
612     * @return the value of this property attached to the structure record, split as a (unmodifiable) list of Strings
613     */
614    public List<String> getStructureValueList() {
615
616        if ((m_structureValueList == null) && (m_structureValue != null)) {
617            // use lazy initializing of the list
618            m_structureValueList = createListFromValue(m_structureValue);
619            m_structureValueList = Collections.unmodifiableList(m_structureValueList);
620        }
621        return m_structureValueList;
622    }
623
624    /**
625     * Returns the value of this property attached to the structure record as a map.<p>
626     * 
627     * This map is build from the used value, which is split into separate key/value pairs
628     * using the <code>|</code> char as delimiter. If the delimiter is not found,
629     * then the map will contain one entry.<p>
630     * 
631     * The key/value pairs are separated with the <code>=</code>.<p>
632     * 
633     * @return the value of this property attached to the structure record, as an (unmodifiable) map of Strings
634     */
635    public Map<String, String> getStructureValueMap() {
636
637        if ((m_structureValueMap == null) && (m_structureValue != null)) {
638            // use lazy initializing of the map
639            m_structureValueMap = createMapFromValue(m_structureValue);
640            m_structureValueMap = Collections.unmodifiableMap(m_structureValueMap);
641        }
642        return m_structureValueMap;
643    }
644
645    /**
646     * Returns the compound value of this property.<p>
647     * 
648     * The value returned is the value of {@link #getStructureValue()}, if it is not <code>null</code>.
649     * Otherwise the value if {@link #getResourceValue()} is returned (which may also be <code>null</code>).<p>
650     * 
651     * @return the compound value of this property
652     */
653    public String getValue() {
654
655        return (m_structureValue != null) ? m_structureValue : m_resourceValue;
656    }
657
658    /**
659     * Returns the compound value of this property, or a specified default value,
660     * if both the structure and resource values are null.<p>
661     * 
662     * In other words, this method returns the defaultValue if this property object 
663     * is the null property (see {@link CmsProperty#getNullProperty()}).<p>
664     * 
665     * @param defaultValue a default value which is returned if both the structure and resource values are <code>null</code>
666     * 
667     * @return the compound value of this property, or the default value
668     */
669    public String getValue(String defaultValue) {
670
671        if (this == CmsProperty.NULL_PROPERTY) {
672            // return the default value if this property is the null property
673            return defaultValue;
674        }
675
676        // somebody might have set both values to null manually
677        // on a property object different from the null property...
678        return (m_structureValue != null) ? m_structureValue : ((m_resourceValue != null)
679        ? m_resourceValue
680        : defaultValue);
681    }
682
683    /**
684     * Returns the compound value of this property, split as a list.<p>
685     * 
686     * This list is build form the used value, which is split into separate values
687     * using the <code>|</code> char as delimiter. If the delimiter is not found,
688     * then the list will contain one entry.<p>
689     * 
690     * The value returned is the value of {@link #getStructureValueList()}, if it is not <code>null</code>.
691     * Otherwise the value of {@link #getResourceValueList()} is returned (which may also be <code>null</code>).<p>
692     * 
693     * @return the compound value of this property, split as a (unmodifiable) list of Strings
694     */
695    public List<String> getValueList() {
696
697        return (m_structureValue != null) ? getStructureValueList() : getResourceValueList();
698    }
699
700    /**
701     * Returns the compound value of this property, split as a list, or a specified default value list,
702     * if both the structure and resource values are null.<p>
703     * 
704     * In other words, this method returns the defaultValue if this property object 
705     * is the null property (see {@link CmsProperty#getNullProperty()}).<p>
706     * 
707     * @param defaultValue a default value list which is returned if both the structure and resource values are <code>null</code>
708     * 
709     * @return the compound value of this property, split as a (unmodifiable) list of Strings
710     */
711    public List<String> getValueList(List<String> defaultValue) {
712
713        if (this == CmsProperty.NULL_PROPERTY) {
714            // return the default value if this property is the null property
715            return defaultValue;
716        }
717
718        // somebody might have set both values to null manually
719        // on a property object different from the null property...
720        return (m_structureValue != null) ? getStructureValueList() : ((m_resourceValue != null)
721        ? getResourceValueList()
722        : defaultValue);
723    }
724
725    /**
726     * Returns the compound value of this property as a map.<p>
727     * 
728     * This map is build from the used value, which is split into separate key/value pairs
729     * using the <code>|</code> char as delimiter. If the delimiter is not found,
730     * then the map will contain one entry.<p>
731     * 
732     * The key/value pairs are separated with the <code>=</code>.<p>
733     * 
734     * The value returned is the value of {@link #getStructureValueMap()}, if it is not <code>null</code>.
735     * Otherwise the value of {@link #getResourceValueMap()} is returned (which may also be <code>null</code>).<p>
736     * 
737     * @return the compound value of this property as a (unmodifiable) map of Strings
738     */
739    public Map<String, String> getValueMap() {
740
741        return (m_structureValue != null) ? getStructureValueMap() : getResourceValueMap();
742    }
743
744    /**
745     * Returns the compound value of this property as a map, or a specified default value map,
746     * if both the structure and resource values are null.<p>
747     * 
748     * In other words, this method returns the defaultValue if this property object 
749     * is the null property (see {@link CmsProperty#getNullProperty()}).<p>
750     * 
751     * @param defaultValue a default value map which is returned if both the structure and resource values are <code>null</code>
752     * 
753     * @return the compound value of this property as a (unmodifiable) map of Strings
754     */
755    public Map<String, String> getValueMap(Map<String, String> defaultValue) {
756
757        if (this == CmsProperty.NULL_PROPERTY) {
758            // return the default value if this property is the null property
759            return defaultValue;
760        }
761
762        // somebody might have set both values to null manually
763        // on a property object different from the null property...
764        return (m_structureValue != null) ? getStructureValueMap() : ((m_resourceValue != null)
765        ? getResourceValueMap()
766        : defaultValue);
767    }
768
769    /**
770     * Returns the hash code of the property, which is based only on the property name, not on the values.<p>
771     * 
772     * The resource and structure values are not taken into consideration for the hashcode generation 
773     * because the {@link #equals(Object)} implementation also does not take these into consideration.<p>
774     *
775     * @return the hash code of the property
776     * 
777     * @see java.lang.Object#hashCode()
778     */
779    @Override
780    public int hashCode() {
781
782        return m_name.hashCode();
783    }
784
785    /**
786     * Checks if the resource value of this property should be deleted when this
787     * property object is written to the database.<p>
788     * 
789     * @return true, if the resource value of this property should be deleted
790     * @see CmsProperty#DELETE_VALUE
791     */
792    public boolean isDeleteResourceValue() {
793
794        return (m_resourceValue == DELETE_VALUE) || ((m_resourceValue != null) && (m_resourceValue.length() == 0));
795    }
796
797    /**
798     * Checks if the structure value of this property should be deleted when this
799     * property object is written to the database.<p>
800     * 
801     * @return true, if the structure value of this property should be deleted
802     * @see CmsProperty#DELETE_VALUE
803     */
804    public boolean isDeleteStructureValue() {
805
806        return (m_structureValue == DELETE_VALUE) || ((m_structureValue != null) && (m_structureValue.length() == 0));
807    }
808
809    /**
810     * Returns <code>true</code> if this property is frozen, that is read only.<p>
811     *
812     * @return <code>true</code> if this property is frozen, that is read only
813     */
814    public boolean isFrozen() {
815
816        return m_frozen;
817    }
818
819    /**
820     * Tests if a given CmsProperty is identical to this CmsProperty object.<p>
821     * 
822     * The property object are identical if their name, structure and 
823     * resource values are all equals.<p>
824     * 
825     * @param property another property object
826     * @return true, if the specified object is equal to this CmsProperty object
827     */
828    public boolean isIdentical(CmsProperty property) {
829
830        boolean isEqual;
831
832        // compare the name
833        if (m_name == null) {
834            isEqual = (property.getName() == null);
835        } else {
836            isEqual = m_name.equals(property.getName());
837        }
838
839        // compare the structure value
840        if (m_structureValue == null) {
841            isEqual &= (property.getStructureValue() == null);
842        } else {
843            isEqual &= m_structureValue.equals(property.getStructureValue());
844        }
845
846        // compare the resource value
847        if (m_resourceValue == null) {
848            isEqual &= (property.getResourceValue() == null);
849        } else {
850            isEqual &= m_resourceValue.equals(property.getResourceValue());
851        }
852
853        return isEqual;
854    }
855
856    /**
857     * Checks if this property object is the null property object.<p>
858     * 
859     * @return true if this property object is the null property object
860     */
861    public boolean isNullProperty() {
862
863        return NULL_PROPERTY.equals(this);
864    }
865
866    /**
867     * Sets the boolean flag to decide if the property definition for this property should be 
868     * created implicitly on any write operation if doesn't exist already.<p>
869     * 
870     * @param value true, if the property definition for this property should be created implicitly on any write operation
871     */
872    public void setAutoCreatePropertyDefinition(boolean value) {
873
874        checkFrozen();
875        m_autoCreatePropertyDefinition = value;
876    }
877
878    /**
879     * Sets the frozen state of the property, if set to <code>true</code> then this property is read only.<p>
880     *
881     * If the property is already frozen, then setting the frozen state to <code>true</code> again is allowed, 
882     * but setting the value to <code>false</code> causes a <code>{@link CmsRuntimeException}</code>.<p>
883     *
884     * @param frozen the frozen state to set
885     */
886    public void setFrozen(boolean frozen) {
887
888        if (!frozen) {
889            checkFrozen();
890        }
891        m_frozen = frozen;
892    }
893
894    /**
895     * Sets the name of this property.<p>
896     * 
897     * @param name the name to set
898     */
899    public void setName(String name) {
900
901        checkFrozen();
902        m_name = name.trim();
903    }
904
905    /**
906     * Sets the path of the resource from which the property was read.<p>
907     * 
908     * @param originRootPath the root path of the root path from which the property was read
909     */
910    public void setOrigin(String originRootPath) {
911
912        checkFrozen();
913        m_origin = originRootPath;
914    }
915
916    /**
917     * Sets the value of this property attached to the resource record.<p>
918     * 
919     * @param resourceValue the value of this property attached to the resource record
920     */
921    public void setResourceValue(String resourceValue) {
922
923        checkFrozen();
924        m_resourceValue = resourceValue;
925        m_resourceValueList = null;
926    }
927
928    /**
929     * Sets the value of this property attached to the resource record from the given list of Strings.<p>
930     * 
931     * The value will be created from the individual values of the given list, which are appended
932     * using the <code>|</code> char as delimiter.<p>
933     * 
934     * @param valueList the list of value (Strings) to attach to the resource record
935     */
936    public void setResourceValueList(List<String> valueList) {
937
938        checkFrozen();
939        if (valueList != null) {
940            m_resourceValueList = new ArrayList<String>(valueList);
941            m_resourceValueList = Collections.unmodifiableList(m_resourceValueList);
942            m_resourceValue = createValueFromList(m_resourceValueList);
943        } else {
944            m_resourceValueList = null;
945            m_resourceValue = null;
946        }
947    }
948
949    /**
950     * Sets the value of this property attached to the resource record from the given map of Strings.<p>
951     * 
952     * The value will be created from the individual values of the given map, which are appended
953     * using the <code>|</code> char as delimiter, the map keys and values are separated by a <code>=</code>.<p>
954     * 
955     * @param valueMap the map of key/value (Strings) to attach to the resource record
956     */
957    public void setResourceValueMap(Map<String, String> valueMap) {
958
959        checkFrozen();
960        if (valueMap != null) {
961            m_resourceValueMap = new HashMap<String, String>(valueMap);
962            m_resourceValueMap = Collections.unmodifiableMap(m_resourceValueMap);
963            m_resourceValue = createValueFromMap(m_resourceValueMap);
964        } else {
965            m_resourceValueMap = null;
966            m_resourceValue = null;
967        }
968    }
969
970    /**
971     * Sets the value of this property attached to the structure record.<p>
972     * 
973     * @param structureValue the value of this property attached to the structure record
974     */
975    public void setStructureValue(String structureValue) {
976
977        checkFrozen();
978        m_structureValue = structureValue;
979        m_structureValueList = null;
980    }
981
982    /**
983     * Sets the value of this property attached to the structure record from the given list of Strings.<p>
984     * 
985     * The value will be created from the individual values of the given list, which are appended
986     * using the <code>|</code> char as delimiter.<p>
987     * 
988     * @param valueList the list of value (Strings) to attach to the structure record
989     */
990    public void setStructureValueList(List<String> valueList) {
991
992        checkFrozen();
993        if (valueList != null) {
994            m_structureValueList = new ArrayList<String>(valueList);
995            m_structureValueList = Collections.unmodifiableList(m_structureValueList);
996            m_structureValue = createValueFromList(m_structureValueList);
997        } else {
998            m_structureValueList = null;
999            m_structureValue = null;
1000        }
1001    }
1002
1003    /**
1004     * Sets the value of this property attached to the structure record from the given map of Strings.<p>
1005     * 
1006     * The value will be created from the individual values of the given map, which are appended
1007     * using the <code>|</code> char as delimiter, the map keys and values are separated by a <code>=</code>.<p>
1008     * 
1009     * @param valueMap the map of key/value (Strings) to attach to the structure record
1010     */
1011    public void setStructureValueMap(Map<String, String> valueMap) {
1012
1013        checkFrozen();
1014        if (valueMap != null) {
1015            m_structureValueMap = new HashMap<String, String>(valueMap);
1016            m_structureValueMap = Collections.unmodifiableMap(m_structureValueMap);
1017            m_structureValue = createValueFromMap(m_structureValueMap);
1018        } else {
1019            m_structureValueMap = null;
1020            m_structureValue = null;
1021        }
1022    }
1023
1024    /**
1025     * Sets the value of this property as either shared or 
1026     * individual value.<p>
1027     * 
1028     * If the given type equals {@link CmsProperty#TYPE_SHARED} then
1029     * the value is set as a shared (resource) value, otherwise it
1030     * is set as individual (structure) value.<p>
1031     * 
1032     * @param value the value to set
1033     * @param type the value type to set
1034     */
1035    public void setValue(String value, String type) {
1036
1037        checkFrozen();
1038        setAutoCreatePropertyDefinition(true);
1039        if (TYPE_SHARED.equalsIgnoreCase(type)) {
1040            // set the provided value as shared (resource) value
1041            setResourceValue(value);
1042        } else {
1043            // set the provided value as individual (structure) value
1044            setStructureValue(value);
1045        }
1046    }
1047
1048    /**
1049     * Returns a string representation of this property object.<p>
1050     * 
1051     * @see java.lang.Object#toString()
1052     */
1053    @Override
1054    public String toString() {
1055
1056        StringBuffer strBuf = new StringBuffer();
1057
1058        strBuf.append("[").append(getClass().getName()).append(": ");
1059        strBuf.append("name: '").append(m_name).append("'");
1060        strBuf.append(", value: '").append(getValue()).append("'");
1061        strBuf.append(", structure value: '").append(m_structureValue).append("'");
1062        strBuf.append(", resource value: '").append(m_resourceValue).append("'");
1063        strBuf.append(", frozen: ").append(m_frozen);
1064        strBuf.append(", origin: ").append(m_origin);
1065        strBuf.append("]");
1066
1067        return strBuf.toString();
1068    }
1069
1070    /**
1071     * Checks if this property is frozen, that is read only.<p> 
1072     */
1073    private void checkFrozen() {
1074
1075        if (m_frozen) {
1076            throw new CmsRuntimeException(Messages.get().container(Messages.ERR_PROPERTY_FROZEN_1, toString()));
1077        }
1078    }
1079
1080    /**
1081     * Returns the list value representation for the given String.<p>
1082     * 
1083     * The given value is split along the <code>|</code> char.<p>
1084     * 
1085     * @param value the value to create the list representation for
1086     * 
1087     * @return the list value representation for the given String
1088     */
1089    private List<String> createListFromValue(String value) {
1090
1091        if (value == null) {
1092            return null;
1093        }
1094        List<String> result = CmsStringUtil.splitAsList(value, VALUE_LIST_DELIMITER);
1095        if (value.indexOf(VALUE_LIST_DELIMITER_REPLACEMENT) != -1) {
1096            List<String> tempList = new ArrayList<String>(result.size());
1097            Iterator<String> i = result.iterator();
1098            while (i.hasNext()) {
1099                String item = i.next();
1100                tempList.add(rebuildDelimiter(item, VALUE_LIST_DELIMITER, VALUE_LIST_DELIMITER_REPLACEMENT));
1101            }
1102            result = tempList;
1103        }
1104
1105        return result;
1106    }
1107
1108    /**
1109     * Returns the map value representation for the given String.<p>
1110     * 
1111     * The given value is split along the <code>|</code> char, the map keys and values are separated by a <code>=</code>.<p>
1112     * 
1113     * @param value the value to create the map representation for
1114     * 
1115     * @return the map value representation for the given String
1116     */
1117    private Map<String, String> createMapFromValue(String value) {
1118
1119        if (value == null) {
1120            return null;
1121        }
1122        List<String> entries = createListFromValue(value);
1123        Iterator<String> i = entries.iterator();
1124        Map<String, String> result = new HashMap<String, String>(entries.size());
1125        boolean rebuildDelimiters = false;
1126        if (value.indexOf(VALUE_MAP_DELIMITER_REPLACEMENT) != -1) {
1127            rebuildDelimiters = true;
1128        }
1129        while (i.hasNext()) {
1130            String entry = i.next();
1131            int index = entry.indexOf(VALUE_MAP_DELIMITER);
1132            if (index != -1) {
1133                String key = entry.substring(0, index);
1134                String val = "";
1135                if ((index + 1) < entry.length()) {
1136                    val = entry.substring(index + 1);
1137                }
1138                if (CmsStringUtil.isNotEmpty(key)) {
1139                    if (rebuildDelimiters) {
1140                        key = rebuildDelimiter(key, VALUE_MAP_DELIMITER, VALUE_MAP_DELIMITER_REPLACEMENT);
1141                        val = rebuildDelimiter(val, VALUE_MAP_DELIMITER, VALUE_MAP_DELIMITER_REPLACEMENT);
1142                    }
1143                    result.put(key, val);
1144                }
1145            }
1146        }
1147        return result;
1148    }
1149
1150    /**
1151     * Returns the single String value representation for the given value list.<p>
1152     * 
1153     * @param valueList the value list to create the single String value for
1154     * 
1155     * @return the single String value representation for the given value list
1156     */
1157    private String createValueFromList(List<String> valueList) {
1158
1159        if (valueList == null) {
1160            return null;
1161        }
1162        StringBuffer result = new StringBuffer(valueList.size() * 32);
1163        Iterator<String> i = valueList.iterator();
1164        while (i.hasNext()) {
1165            result.append(replaceDelimiter(i.next().toString(), VALUE_LIST_DELIMITER, VALUE_LIST_DELIMITER_REPLACEMENT));
1166            if (i.hasNext()) {
1167                result.append(VALUE_LIST_DELIMITER);
1168            }
1169        }
1170        return result.toString();
1171    }
1172
1173    /**
1174     * Returns the single String value representation for the given value map.<p>
1175     * 
1176     * @param valueMap the value map to create the single String value for
1177     * 
1178     * @return the single String value representation for the given value map
1179     */
1180    private String createValueFromMap(Map<String, String> valueMap) {
1181
1182        if (valueMap == null) {
1183            return null;
1184        }
1185        StringBuffer result = new StringBuffer(valueMap.size() * 32);
1186        Iterator<Map.Entry<String, String>> i = valueMap.entrySet().iterator();
1187        while (i.hasNext()) {
1188            Map.Entry<String, String> entry = i.next();
1189            String key = entry.getKey();
1190            String value = entry.getValue();
1191            key = replaceDelimiter(key, VALUE_LIST_DELIMITER, VALUE_LIST_DELIMITER_REPLACEMENT);
1192            key = replaceDelimiter(key, VALUE_MAP_DELIMITER, VALUE_MAP_DELIMITER_REPLACEMENT);
1193            value = replaceDelimiter(value, VALUE_LIST_DELIMITER, VALUE_LIST_DELIMITER_REPLACEMENT);
1194            value = replaceDelimiter(value, VALUE_MAP_DELIMITER, VALUE_MAP_DELIMITER_REPLACEMENT);
1195            result.append(key);
1196            result.append(VALUE_MAP_DELIMITER);
1197            result.append(value);
1198            if (i.hasNext()) {
1199                result.append(VALUE_LIST_DELIMITER);
1200            }
1201        }
1202        return result.toString();
1203    }
1204
1205    /**
1206     * Rebuilds the given delimiter character from the replacement string.<p>
1207     * 
1208     * @param value the string that is scanned
1209     * @param delimiter the delimiter character to rebuild
1210     * @param delimiterReplacement the replacement string for the delimiter character
1211     * @return the substituted string
1212     */
1213    private String rebuildDelimiter(String value, char delimiter, String delimiterReplacement) {
1214
1215        return CmsStringUtil.substitute(value, delimiterReplacement, String.valueOf(delimiter));
1216    }
1217
1218    /**
1219     * Replaces the given delimiter character with the replacement string.<p>
1220     *  
1221     * @param value the string that is scanned
1222     * @param delimiter the delimiter character to replace
1223     * @param delimiterReplacement the replacement string for the delimiter character
1224     * @return the substituted string
1225     */
1226    private String replaceDelimiter(String value, char delimiter, String delimiterReplacement) {
1227
1228        return CmsStringUtil.substitute(value, String.valueOf(delimiter), delimiterReplacement);
1229    }
1230}