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