Java tutorial
/*************************************************************** * This file is part of the [fleXive](R) framework. * * Copyright (c) 1999-2014 * UCS - unique computing solutions gmbh (http://www.ucs.at) * All rights reserved * * The [fleXive](R) project is free software; you can redistribute * it and/or modify it under the terms of the GNU Lesser General Public * License version 2.1 or higher as published by the Free Software Foundation. * * The GNU Lesser General Public License can be found at * http://www.gnu.org/licenses/lgpl.html. * A copy is found in the textfile LGPL.txt and important notices to the * license from the author are found in LICENSE.txt distributed with * these libraries. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * For further information about UCS - unique computing solutions gmbh, * please see the company website: http://www.ucs.at * * For further information about [fleXive](R), please see the * project website: http://www.flexive.org * * * This copyright notice MUST APPEAR in all copies of the file! ***************************************************************/ package com.flexive.shared.value; import com.flexive.shared.*; import com.flexive.shared.content.FxValueChangeListener; import com.flexive.shared.content.FxValueChangeListener.ChangeType; import com.flexive.shared.exceptions.FxInvalidParameterException; import com.flexive.shared.exceptions.FxInvalidStateException; import com.flexive.shared.exceptions.FxRuntimeException; import com.flexive.shared.security.UserTicket; import com.flexive.shared.structure.FxDataType; import com.flexive.shared.structure.FxPropertyAssignment; import com.flexive.shared.value.renderer.FxValueRendererFactory; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.primitives.Longs; import org.apache.commons.lang.StringUtils; import java.io.Serializable; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * Abstract base class of all value objects. * Common base classed is used for multilingual properties, etc. * <p/> * To check if a value is empty a flag is used for each language resp. the single value. * Use the setEmpty() method to explicitly set a value to be empty * * @author Markus Plesser (markus.plesser@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at) */ @SuppressWarnings("UnusedDeclaration") public abstract class FxValue<T, TDerived extends FxValue<T, TDerived>> implements Serializable, Comparable<FxValue> { private static final long serialVersionUID = -5005063788615664383L; public static final boolean DEFAULT_MULTILANGUAGE = true; public static final Integer VALUE_NODATA = null; private static final long[] SYSTEM_LANG_ARRAY = new long[] { FxLanguage.SYSTEM_ID }; private static final EmptyTranslation EMPTY_TRANSLATION = new EmptyTranslation(); protected long defaultLanguage = FxLanguage.SYSTEM_ID; private String XPath = "", xpathPrefix = ""; private Integer valueData = VALUE_NODATA; private FxValueChangeListener changeListener = null; /** * Data if <code>multiLanguage</code> is enabled */ protected Map<Long, T> translations; /** * Value data for each language */ protected Map<Long, Integer> multiLangData = null; /** * Data if <code>multiLanguage</code> is disabled */ protected T singleValue; private boolean singleValueEmpty; private boolean readOnly; /** * Constructor * * @param multiLanguage multilanguage value? * @param defaultLanguage the default language * @param translations HashMap containing language->translation mapping */ protected FxValue(boolean multiLanguage, long defaultLanguage, Map<Long, T> translations) { this.defaultLanguage = defaultLanguage; this.readOnly = false; if (multiLanguage) { if (translations == null) { //valid to pass null, create an empty one this.translations = new HashMap<Long, T>(4); } else { this.translations = new HashMap<Long, T>(translations); } if (this.defaultLanguage < 0) { this.defaultLanguage = FxLanguage.SYSTEM_ID; } } else { if (translations != null && !translations.isEmpty()) { //a translation is provided, use the defaultLanguage element or very first element if not present singleValue = removeEmptyMark(translations.get(defaultLanguage)); if (singleValue == null) singleValue = translations.values().iterator().next(); } this.defaultLanguage = FxLanguage.SYSTEM_ID; this.translations = null; this.singleValueEmpty = false; } } /** * Initialize an empty FxValue (used for initalization for XML import, etc.) * * @param defaultLanguage default language * @param multiLanguage multilanguage value? */ protected FxValue(long defaultLanguage, boolean multiLanguage) { this.defaultLanguage = defaultLanguage; this.readOnly = false; if (multiLanguage) { this.translations = new HashMap<Long, T>(4); if (this.defaultLanguage < 0) { this.defaultLanguage = FxLanguage.SYSTEM_ID; } } else { this.defaultLanguage = FxLanguage.SYSTEM_ID; this.translations = null; this.singleValueEmpty = false; } } /** * Constructor * * @param defaultLanguage the default language * @param translations HashMap containing language->translation mapping */ protected FxValue(long defaultLanguage, Map<Long, T> translations) { this(DEFAULT_MULTILANGUAGE, defaultLanguage, translations); } /** * Constructor * * @param multiLanguage multilanguage value? * @param translations HashMap containing language->translation mapping */ protected FxValue(boolean multiLanguage, Map<Long, T> translations) { this(multiLanguage, FxLanguage.SYSTEM_ID, translations); } /** * Constructor * * @param translations HashMap containing language->translation mapping */ protected FxValue(Map<Long, T> translations) { this(DEFAULT_MULTILANGUAGE, FxLanguage.SYSTEM_ID, translations); } /** * Constructor - create value from an array of translations * * @param translations HashMap containing language->translation mapping * @param pos position (index) in the array to use */ protected FxValue(Map<Long, T[]> translations, int pos) { this(DEFAULT_MULTILANGUAGE, FxLanguage.SYSTEM_ID, new HashMap<Long, T>((translations == null ? 5 : translations.size()))); if (translations == null) return; for (Entry<Long, T[]> e : translations.entrySet()) if (e.getValue()[pos] != null) this.translations.put(e.getKey(), e.getValue()[pos]); else this.translations.put(e.getKey(), null); } /** * Constructor * * @param multiLanguage multilanguage value? * @param defaultLanguage the default language and the language for the value * @param value single initializing value */ protected FxValue(boolean multiLanguage, long defaultLanguage, T value) { //noinspection RedundantCast this(multiLanguage, defaultLanguage, (Map<Long, T>) null); if (value == null) { if (multiLanguage) markEmpty(defaultLanguage); else this.singleValueEmpty = true; } else { if (multiLanguage) this.translations.put(defaultLanguage, value); else this.singleValue = value; } } /** * Constructor * * @param defaultLanguage the default language * @param value single initializing value */ protected FxValue(long defaultLanguage, T value) { this(DEFAULT_MULTILANGUAGE, defaultLanguage, value); } /** * Constructor * * @param multiLanguage multilanguage value? * @param value single initializing value */ protected FxValue(boolean multiLanguage, T value) { this(multiLanguage, FxLanguage.DEFAULT_ID, value); } /** * Constructor * * @param value single initializing value */ protected FxValue(T value) { this(DEFAULT_MULTILANGUAGE, value); } /** * Constructor * * @param clone original FxValue to be cloned */ @SuppressWarnings("unchecked") protected FxValue(FxValue<T, TDerived> clone) { this(clone.isMultiLanguage(), clone.getDefaultLanguage(), new HashMap<Long, T>((clone.translations != null ? clone.translations.size() : 1))); this.XPath = clone.XPath; this.xpathPrefix = clone.xpathPrefix; this.valueData = clone.valueData; if (clone.multiLangData != null) this.multiLangData = Maps.newHashMap(clone.multiLangData); this.changeListener = clone.changeListener; if (clone.isImmutableValueType()) { if (clone.isMultiLanguage()) { // clone only hashmap this.translations = new HashMap(clone.translations); } else { this.singleValue = clone.singleValue; this.singleValueEmpty = clone.singleValueEmpty; } } else { if (clone.isMultiLanguage()) { for (long k : clone.translations.keySet()) { T t = removeEmptyMark(clone.translations.get(k)); if (t == null) { this.translations.put(k, null); } else { this.translations.put(k, t == null ? null : copyValue(t)); } } } else { this.singleValue = clone.singleValue == null ? null : copyValue(clone.singleValue); this.singleValueEmpty = clone.singleValueEmpty; } } } /** * Get the XPath for this value - the XPath is optional and can be an empty String if * not explicitly assigned! * * @return XPath (optional! can be an empty String) */ public String getXPath() { return xpathPrefix == null || xpathPrefix.length() == 0 ? XPath : xpathPrefix + XPath; } /** * Returns the name of the value from the xpath. * <p/> * If the xpath is an empty string the name will also return an emptry String. * * @return the property name */ public String getXPathName() { try { String xpathSplit[] = getXPath().split("/"); return xpathSplit[xpathSplit.length - 1].split("\\[")[0]; } catch (Throwable t) { return ""; } } /** * Set the XPath (unless readonly) * * @param XPath the XPath to set, will be ignored if readonly * @return this */ @SuppressWarnings({ "unchecked" }) public TDerived setXPath(String XPath) { if (!this.readOnly) { this.XPath = XPath != null ? XPathElement.xpToUpperCase(XPath) : null; this.xpathPrefix = ""; } return (TDerived) this; } /** * Set the XPath (unless readonly) * * @param xpathPrefix the xpath prefix (e.g. instance PK or type) * @param xpath the XPath * @return this * @since 3.2.0 */ @SuppressWarnings("unchecked") public TDerived setXPath(String xpathPrefix, String xpath) { if (!this.readOnly) { this.XPath = StringUtils.isBlank(xpath) ? "" : XPathElement.xpToUpperCase(xpath); this.xpathPrefix = StringUtils.isBlank(xpathPrefix) ? "" : XPathElement.xpToUpperCase(xpathPrefix); } return (TDerived) this; } /** * Copy the internal "meta" properties associated with a value into this instance (XPath, value data). * * @param other the instance to copy the data from * @param copyValueData if the value data should be copied as well (optional since technically the valueData is * not an internal bookkeeping property, but associated with the wrapped value). * @since 3.2.1 */ @SuppressWarnings("unchecked") public void setInternalProperties(FxValue other, boolean copyValueData) { this.XPath = other.XPath; this.xpathPrefix = other.xpathPrefix; this.readOnly = other.readOnly; this.changeListener = other.changeListener; if (copyValueData) { this.valueData = other.valueData; this.multiLangData = other.multiLangData == null ? null : Maps.<Long, Integer>newHashMap(other.multiLangData); } } /** * One-time operation to flag this FxValue as read only. * This is not reversible! */ public void setReadOnly() { this.readOnly = true; } /** * Mark this FxValue as empty * * @return this */ @SuppressWarnings("unchecked") public TDerived setEmpty() { if (isMultiLanguage()) { for (Long lang : this.translations.keySet()) markEmpty(lang); } else { this.singleValueEmpty = true; } if (this.changeListener != null) { this.changeListener.onValueChanged(getXPath(), ChangeType.Remove); } return (TDerived) this; } /** * Mark the entry for the given language as empty * * @param language the language to flag as empty */ public void setEmpty(long language) { if (isMultiLanguage()) { markEmpty(language); } else { this.singleValueEmpty = true; } if (this.changeListener != null) { if (isEmpty()) { this.changeListener.onValueChanged(getXPath(), ChangeType.Remove); } } } /** * Return the class instance of the value type. * * @return the class instance of the value type. */ public abstract Class<T> getValueClass(); /** * Evaluates the given string value to an object of type T. * * @param value string value to be evaluated * @return the value interpreted as T */ public abstract T fromString(String value); /** * Convert from a portable (not locale specific format) * * @param value portable string value to be evaluated * @return the value interpreted as T * @since 3.1.6 */ public T fromPortableString(String value) { return fromString(value); } /** * Converts the given instance of T to a string that can be * parsed again by {@link FxValue#fromString(String)}. * * @param value the value to be converted * @return a string representation of the given value that can be parsed again using * {@link FxValue#fromString(String)}. */ public String getStringValue(T value) { return String.valueOf(value); } /** * Converts the given instance of T to a string that can be * parsed again by {@link FxValue#fromPortableString(String)}. * * @param value the value to be converted * @return a string representation of the given value that can be parsed again using * {@link FxValue#fromPortableString(String)}. * @since 3.1.6 */ public String getPortableStringValue(T value) { return getStringValue(value); } /** * Creates a copy of the given object (useful if the actual type is unknown). * * @return a copy of the given object (useful if the actual type is unknown). */ public abstract TDerived copy(); /** * Implement this method for data types that return false from {@link #isImmutableValueType()}. * * <p> * The default implementation returns the argument as-is and throws an IllegalArgumentException * when the container class has mutable value types. * </p> * * @param value the value to be copied (not null) * @return an independent copy of {@code value} * @since 3.2.0 */ protected T copyValue(T value) { if (!isImmutableValueType()) { throw new IllegalArgumentException( "Mutable datatype, but no implementation of copyValue provided: " + getClass()); } return value; } /** * Return true if T is immutable (e.g. java.lang.String). This prevents cloning * of the translations in copy constructors. * * @return true if T is immutable (e.g. java.lang.String) */ public boolean isImmutableValueType() { return false; } /** * Is this value editable by the user? * This always returns true except it is a FxNoAccess value or flagged as readOnly * * @return if this value editable? * @see FxNoAccess */ public boolean isReadOnly() { return readOnly; } /** * Returns true if this value is valid for the actual type (e.g. if * a FxNumber property actually contains only valid numbers). * * @return true if this value is valid for the actual type */ public boolean isValid() { return _getErrorValue() == null; } /** * Returns true if the translation for the given language is valid. An empty translation * is always valid. * * @param languageId the language ID * @return true if the translation for the given language is valid * @since 3.1 */ public boolean isValid(long languageId) { final T value = getTranslation(languageId); if (value == null || !(value instanceof String)) { // empty or non-string translations are always valid return true; } // try a conversion to the native type try { fromString((String) value); return true; } catch (Exception e) { return false; } } /** * Returns true if the translation for the given language is valid. An empty translation * is always valid. * * @param language the language * @return true if the translation for the given language is valid * @since 3.1 */ public boolean isValid(FxLanguage language) { return isValid(language != null ? language.getId() : -1); } /** * Returns the value that caused {@link #isValid} to return false. If isValid() is true, * a RuntimeException is thrown. * * @return the value that caused the validation via {@link #isValid} to fail * @throws IllegalStateException if the instance is valid and the error value is undefined */ @SuppressWarnings({ "UnusedCatchParameter" }) public T getErrorValue() throws IllegalStateException { final T val = _getErrorValue(); if (val == null) { // no error value is defined throw new IllegalStateException(); } return val; } /** * Return the value that failed to validate. * * @return the error value, or null if this FxValue instance is valid. */ private T _getErrorValue() { if (isMultiLanguage()) { for (T translation : translations.values()) { if (translation instanceof String) { // if a string was used, check if it is a valid representation of our type try { fromString((String) translation); } catch (Exception e) { return translation; } } } } else if (singleValue instanceof String) { try { fromString((String) singleValue); } catch (Exception e) { return singleValue; } } return null; } /** * Get a representation of this value in the default translation * * @return T */ public T getDefaultTranslation() { if (!isMultiLanguage()) return singleValue; T def = getTranslation(getDefaultLanguage()); if (def != null) return def; if (translations.size() > 0) return removeEmptyMark(translations.values().iterator().next()); //first available translation if default does not exist return null; //empty as last fallback } /** * Get the translation for a requested language * * @param lang requested language * @return translation or an empty String if it does not exist */ public T getTranslation(long lang) { return (isMultiLanguage() ? removeEmptyMark(translations.get(lang)) : singleValue); } /** * Get a String representation of this value in the requested language or * an empty String if the translation does not exist * * @param lang requested language id * @return T translation */ public T getTranslation(FxLanguage lang) { if (!isMultiLanguage()) //redundant but faster return singleValue; return getTranslation((int) lang.getId()); } /** * Get the translation that best fits the requested language. * The requested language is queried and if it does not exist the * default translation is returned * * @param lang requested best-fit language * @return best fit translation */ public T getBestTranslation(long lang) { if (!isMultiLanguage()) //redundant but faster return singleValue; T ret = getTranslation(lang); return ret != null ? ret : getDefaultTranslation(); } /** * Get the translation that best fits the requested language. * The requested language is queried and if it does not exist the * default translation is returned * * @param language requested best-fit language * @return best fit translation */ public T getBestTranslation(FxLanguage language) { if (!isMultiLanguage()) //redundant but faster return singleValue; if (language == null) // user ticket language return getBestTranslation(); return getBestTranslation((int) language.getId()); } /** * Get the translation that best fits the requested users language. * The requested users language is queried and if it does not exist the * default translation is returned * * @param ticket UserTicket to obtain the users language * @return best fit translation */ public T getBestTranslation(UserTicket ticket) { if (!isMultiLanguage()) //redundant but faster return singleValue; return getBestTranslation((int) ticket.getLanguage().getId()); } /** * Get the translation that best fits the current users language. * The user language is obtained from the FxContext thread local. * * @return best fit translation */ public T getBestTranslation() { if (!isMultiLanguage()) //redundant but faster return singleValue; return getBestTranslation(FxContext.getUserTicket().getLanguage()); } /** * Get all languages for which translations exist * * @return languages for which translations exist */ public long[] getTranslatedLanguages() { if (isMultiLanguage()) { final List<Long> languages = Lists.newArrayListWithCapacity(translations.size()); for (Entry<Long, T> entry : translations.entrySet()) { if (!valueEmpty(entry.getValue())) { languages.add(entry.getKey()); } } return Longs.toArray(languages); } else { return SYSTEM_LANG_ARRAY.clone(); } } /** * Does a translation exist for the given language? * * @param languageId language to query * @return translation exists */ public boolean translationExists(long languageId) { return !isMultiLanguage() || (translations.get(languageId) != null && !isMarkedEmpty(languageId)); } /** * Like empty(), for JSF EL, since empty cannot be used. * * @return true if the value is empty */ public boolean getIsEmpty() { return isEmpty(); } /** * Is this value empty? * * @return if value is empty */ public boolean isEmpty() { if (isMultiLanguage()) { if (translations.isEmpty()) { return true; } for (Long lang : translations.keySet()) { if (!isMarkedEmpty(lang)) { return false; } } return true; } else return singleValueEmpty; } /** * Check if the translation for the given language is empty * * @param lang language to check * @return if translation for the given language is empty */ public boolean isTranslationEmpty(FxLanguage lang) { return lang != null ? isTranslationEmpty(lang.getId()) : isEmpty(); } /** * Check if the translation for the given language is empty * * @param lang language to check * @return if translation for the given language is empty */ public boolean isTranslationEmpty(long lang) { if (!isMultiLanguage()) return singleValueEmpty; return isMarkedEmpty(lang); } /** * Set the translation for a language or override the single language value if * this value is not flagged as multi language enabled. This method cannot be * overridden since it not only accepts parameters of type T, but also of type * String for web form handling. * * @param language language to set the translation for * @param value translation * @return this */ @SuppressWarnings({ "ThrowableInstanceNeverThrown" }) public final TDerived setTranslation(long language, T value) { if (value instanceof FxValue) { throw new FxInvalidParameterException("value", "ex.content.invalid.translation.fxvalue", value.getClass().getCanonicalName()).asRuntimeException(); } if (value instanceof String) { try { value = this.fromString((String) value); } catch (Exception e) { // do nothing. The resulting FxValue will be invalid, // but the invalid value will be preserved. // TODO: use a "safer" concept of representing invalid translations, // since this may lead to unexpeced ClassCastExceptions in parameterized // methods expecting a <T> value } } FxValueChangeListener.ChangeType change = null; final String xpath = getXPath(); if (!isMultiLanguage()) { if (value == null && !isAcceptsEmptyDefaultTranslations()) { throw new FxInvalidParameterException("value", "ex.content.invalid.default.empty", getClass().getSimpleName()).asRuntimeException(); } if (StringUtils.isNotBlank(xpath) && this.changeListener != null) { if (value != null) { if (this.singleValueEmpty) change = FxValueChangeListener.ChangeType.Add; else if (!this.singleValue.equals(value)) change = FxValueChangeListener.ChangeType.Update; } else if (!this.singleValueEmpty) { change = FxValueChangeListener.ChangeType.Remove; } } //override the single value if (singleValue == null || !singleValue.equals(value)) this.singleValue = value; this.singleValueEmpty = value == null; if (changeListener != null && change != null) changeListener.onValueChanged(xpath, change); //noinspection unchecked return (TDerived) this; } if (translations == null) { //create an empty one, not yet initialized this.translations = new HashMap<Long, T>(4); } if (language == FxLanguage.SYSTEM_ID) throw new FxInvalidParameterException("language", "ex.content.value.invalid.multilanguage.sys") .asRuntimeException(); if (StringUtils.isNotBlank(xpath) && this.changeListener != null && value != null) { if (this.isEmpty()) change = FxValueChangeListener.ChangeType.Add; else { if (!value.equals(translations.get(language))) change = FxValueChangeListener.ChangeType.Update; } } boolean wasEmpty = this.changeListener != null && isEmpty(); //only evaluate if we have a change listener attached if (value == null) { translations.remove(language); } else { if (!value.equals(translations.get(language))) { translations.put(language, value); } } if (StringUtils.isNotBlank(xpath) && this.changeListener != null && value == null && !wasEmpty && isEmpty()) { change = FxValueChangeListener.ChangeType.Remove; } if (changeListener != null && change != null) changeListener.onValueChanged(xpath, change); //noinspection unchecked return (TDerived) this; } /** * Set the translation for a language or override the single language value if * this value is not flagged as multi language enabled * * @param lang language to set the translation for * @param translation translation * @return this */ public TDerived setTranslation(FxLanguage lang, T translation) { return setTranslation((int) lang.getId(), translation); } /** * For multilanguage values, set the default translation. * For single language values, set the value. * * @param value the value to be stored */ public void setValue(T value) { setTranslation(getDefaultLanguage(), value); } /** * Set the translation in the default language. For single-language values, * sets the value. * * @param translation the default translation * @return this */ public FxValue setDefaultTranslation(T translation) { return setTranslation(defaultLanguage, translation); } /** * Get the default language of this value * * @return default language */ public long getDefaultLanguage() { if (!isMultiLanguage()) return FxLanguage.SYSTEM_ID; return this.defaultLanguage; } /** * Returns the maximum input length an input field should have for this value * (or -1 for unlimited length). * * @return the maximum input length an input field should have for this value */ public int getMaxInputLength() { final String xp = getXPath(); if (StringUtils.isBlank(xp)) { return -1; } else { try { final FxPropertyAssignment pa = CacheAdmin.getEnvironment().getPropertyAssignment(xp); final int maxLength = pa.getMaxLength(); if (maxLength > 0) { return maxLength; } final FxDataType dataType = pa.getProperty().getDataType(); return dataType == FxDataType.String1024 ? 1024 : -1; } catch (FxRuntimeException e) { return -1; } } } /** * Set the default language. * It will only be set if a translation in the requested default language * exists! * * @param defaultLanguage requested default language */ public void setDefaultLanguage(long defaultLanguage) { setDefaultLanguage(defaultLanguage, false); } /** * Set the default language. Will have no effect if the value is not multi language enabled * * @param defaultLanguage requested default language * @param force if true, the default language will also be updated if no translation exists (for UI input) */ public void setDefaultLanguage(long defaultLanguage, boolean force) { if (isMultiLanguage() && (force || translationExists(defaultLanguage))) { this.defaultLanguage = defaultLanguage; } } /** * Reset the default language to the system language */ public void clearDefaultLanguage() { this.defaultLanguage = FxLanguage.SYSTEM_ID; } /** * Is a default value set for this FxValue? * * @return default value set */ public boolean hasDefaultLanguage() { return defaultLanguage != FxLanguage.SYSTEM_ID && isMultiLanguage(); } /** * Check if the passed language is the default language * * @param language the language to check * @return passed language is the default language */ public boolean isDefaultLanguage(long language) { return !isMultiLanguage() && language == FxLanguage.SYSTEM_ID || hasDefaultLanguage() && language == defaultLanguage; } /** * Remove the translation for the given language * * @param language the language to remove the translation for */ public void removeLanguage(long language) { if (!isMultiLanguage()) { setEmpty(); // ensure that the old value is not "leaked" to clients that don't check isEmpty() // and that the behaviour is consistent with multi-language inputs (FX-485) singleValue = getEmptyValue(); } else { translations.remove(language); } } /** * Is this value available for multiple languages? * * @return value available for multiple languages */ public boolean isMultiLanguage() { return this.translations != null; } protected boolean isAcceptsEmptyDefaultTranslations() { return true; } /** * Format this FxValue for inclusion in a SQL statement. For example, * a string is wrapped in single quotes and escaped properly (' --> ''). * For multilanguage values the default translation is used. If the value is * empty (@link #isEmpty()), a runtime exception is thrown. * * @return the formatted value */ @SuppressWarnings({ "ThrowableInstanceNeverThrown" }) public String getSqlValue() { if (isEmpty()) { throw new FxInvalidStateException("ex.content.value.sql.empty").asRuntimeException(); } return FxFormatUtils.escapeForSql(getDefaultTranslation()); } /** * Returns an empty value object for this FxValue type. * * @return an empty value object for this FxValue type. */ public abstract T getEmptyValue(); /** * {@inheritDoc} */ @Override public String toString() { // format value in the current user's locale - also used in the JSF UI return FxValueRendererFactory.getInstance().format(this); } /** * {@inheritDoc} */ @Override public boolean equals(Object other) { if (this == other) return true; if (other == null) return false; if (this.getClass() != other.getClass()) return false; FxValue<?, ?> otherValue = (FxValue<?, ?>) other; if (this.isEmpty() != otherValue.isEmpty()) return false; if (!equalsValueData(otherValue)) return false; if (this.isMultiLanguage() != otherValue.isMultiLanguage()) return false; if (isMultiLanguage()) { for (Entry<Long, T> entry : this.translations.entrySet()) { final Long key = entry.getKey(); if (valueEmpty(entry.getValue())) { if (!valueEmpty(otherValue.translations.get(key))) { return false; } } else { final T value = entry.getValue(); if (!value.equals(otherValue.translations.get(key))) { return false; } } } for (Entry<Long, ?> otherEntry : otherValue.translations.entrySet()) { if (!valueEmpty(otherEntry.getValue()) && isTranslationEmpty(otherEntry.getKey())) { return false; } } } else { if (!this.isEmpty()) if (!this.singleValue.equals(otherValue.singleValue)) return false; } return true; } /** * Compare value data with another FxValue for equality * * @param otherValue other FxValue to compare * @return equal */ private boolean equalsValueData(FxValue<?, ?> otherValue) { if (hasValueData() != otherValue.hasValueData()) return false; if (isMultiLanguage() != otherValue.isMultiLanguage()) return false; if (!hasValueData()) return true; if (isMultiLanguage()) { // check if value data is equal, taking into account that the "default value" (the main valueData field) can // be used as fallback. The other direction (otherValue -> this) does not need to be checked, because if the number // of translations does not match, the equality check will return false anyway. final Map<Long, Integer> thisML = multiLangData != null ? multiLangData : Collections.<Long, Integer>emptyMap(); final Map<Long, Integer> otherML = otherValue.multiLangData != null ? otherValue.multiLangData : Collections.<Long, Integer>emptyMap(); for (Entry<Long, Integer> entry : thisML.entrySet()) { final Integer value = entry.getValue(); final Integer other = otherML.get(entry.getKey()); final Integer checkValue = value == null ? valueData : value; final Integer checkOther = other == null ? otherValue.valueData : other; if (checkValue != null && !checkValue.equals(checkOther)) { return false; } else if (checkValue == null && checkOther != null) { return false; } } return true; } else { return valueData.equals(otherValue.valueData); } } /** * {@inheritDoc} */ @Override public int hashCode() { int hash = 7; if (translations != null) hash = 31 * hash + translations.hashCode(); hash = 31 * hash + (int) defaultLanguage; if (hasValueData()) { if (isMultiLanguage()) for (Integer val : multiLangData.values()) hash += val; else hash += getValueDataRaw(); } return hash; } /** * A generic comparable implementation based on the value's string representation. * * @param o the other object * @return see {@link Comparable#compareTo}. */ @Override @SuppressWarnings({ "unchecked", "NullableProblems" }) public int compareTo(FxValue o) { if (o == null) { return 1; } if (isEmpty() && !o.isEmpty()) { return -1; } if (isEmpty()) { return 0; } if (o.isEmpty()) { return 1; } final String value = getStringValue(getBestTranslation()); final String oValue = o.getStringValue(o.getBestTranslation()); if (value == null && oValue == null) { return 0; } else if (value == null) { return -1; } else if (oValue == null) { return 1; } else { return FxSharedUtils.getCollator().compare(value, oValue); } } /** * Get attached value data (optional, if not set will return <code>VALUE_NODATA</code>). * As value data might contain some bit-coded flags in the future, it is not certain if the full Integer range * will be available as some bits might be masked out. * * @return attached value data, if not set will return <code>VALUE_NODATA</code> * @since 3.1.4 */ public Integer getValueData() { return getValueDataRaw(); } /** * Get attached value data (optional, if not set will return <code>VALUE_NODATA</code>) including any bit-coded flags. * Internal use only! * * @return raw attached value data, if not set will return <code>VALUE_NODATA</code> * @since 3.1.4 */ public Integer getValueDataRaw() { if (isMultiLanguage()) { if (multiLangData == null || multiLangData.isEmpty()) return valueData; //fall back to the first available entry return multiLangData.values().iterator().next(); } return valueData; } /** * Get attached language specific value data (optional, if not set will return <code>VALUE_NODATA</code>) including any bit-coded flags. * Internal use only! * * @param language fetch the value data for this language * @return raw attached value data, if not set will return <code>VALUE_NODATA</code> * @since 3.2 */ public Integer getValueDataRaw(long language) { if (isMultiLanguage()) { if (multiLangData == null || !multiLangData.containsKey(language)) return valueData; return multiLangData.get(language); } else return valueData; } /** * Attach additional data to this value instance * * @param valueData value data to attach * @return this * @since 3.1.4 */ @SuppressWarnings({ "unchecked" }) public TDerived setValueData(Integer valueData) { if (isMultiLanguage()) { if (this.translations != null) { // keep "global" flag for languages that may be added in the future this.valueData = valueData; if (valueData == null) { this.multiLangData = null; } else { if (this.multiLangData != null) { // replace existing "custom" values for (Entry<Long, Integer> entry : multiLangData.entrySet()) { entry.setValue(valueData); } } } } } else this.valueData = valueData; return (TDerived) this; } /** * Attach additional data per language to this value instance * * @param language language to attach data for * @param valueData value data to attach * @return this * @since 3.2.1 */ public TDerived setValueData(long language, Integer valueData) { return setValueData(language, valueData, false); } /** * Attach additional data per language to this value instance * * @param language language to attach data for * @param valueData value data to attach * @param allowUseAsFallback if the value may be used as a fallback for other languages (only relevant when no other value data is set) * @return this * @since 3.2.1 */ public TDerived setValueData(long language, Integer valueData, boolean allowUseAsFallback) { if (isMultiLanguage()) { if (multiLangData != null && multiLangData.containsKey(language)) { // clean reset of the default value to avoid losing the association with // the fallback valueData instance - which needs to be cleared if this value was the last one clearValueData(language); } if (this.valueData == null && allowUseAsFallback) { this.valueData = valueData; } if (valueData != null) { final Integer specValue = multiLangData != null ? multiLangData.get(language) : null; if (specValue != null) { // replace existing value multiLangData.put(language, valueData); } else if (!valueData.equals(this.valueData) || multiLangData == null || multiLangData.isEmpty()) { // store only "non-default" flags (and the initial value) in the hashmap if (multiLangData == null) multiLangData = Maps.newHashMap(); multiLangData.put(language, valueData); } } } else this.valueData = valueData; //noinspection unchecked return (TDerived) this; } /** * Unset value data * @since 3.1.4 */ public void clearValueData() { this.valueData = VALUE_NODATA; this.multiLangData = null; } /** * Unset value data for a language * @param language language to clear value data for * @since 3.2 */ public void clearValueData(long language) { if (this.multiLangData != null && this.multiLangData.containsKey(language)) { final Integer oldValue = this.multiLangData.remove(language); if (this.multiLangData.isEmpty()) { this.multiLangData = null; if (oldValue != null && oldValue.equals(this.valueData)) { this.valueData = VALUE_NODATA; } } } else if (multiLangData == null || multiLangData.isEmpty()) { // reset only when there are no other values this.multiLangData = null; this.valueData = VALUE_NODATA; } } /** * Are additional value data set for this value instance? * * @return value data set * @since 3.1.4 */ public boolean hasValueData() { return this.valueData != null || (isMultiLanguage() && multiLangData != null && !multiLangData.isEmpty()); } /** * Are additional value data set for this value instance in the requested language? * * @return value data set * @since 3.2 */ public boolean hasValueData(long language) { return isMultiLanguage() ? valueData != null || (multiLangData != null && multiLangData.containsKey(language) && multiLangData.get(language) != null) : valueData != null; } /** * Set the change listener * * @param changeListener change listener * @since 3.1.6 */ public void setChangeListener(FxValueChangeListener changeListener) { this.changeListener = changeListener; } @SuppressWarnings("unchecked") private void markEmpty(Long language) { ((Map) translations).put(language, EMPTY_TRANSLATION); } private boolean isMarkedEmpty(Long language) { final Object value = ((Map) translations).get(language); return valueEmpty(value); } private boolean valueEmpty(Object value) { return value == null || value instanceof EmptyTranslation; } @SuppressWarnings("unchecked") private T removeEmptyMark(Object value) { return value instanceof EmptyTranslation ? null : (T) value; } private static class EmptyTranslation implements Serializable { } }