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