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