0001: /***************************************************************
0002: * This file is part of the [fleXive](R) project.
0003: *
0004: * Copyright (c) 1999-2008
0005: * UCS - unique computing solutions gmbh (http://www.ucs.at)
0006: * All rights reserved
0007: *
0008: * The [fleXive](R) project is free software; you can redistribute
0009: * it and/or modify it under the terms of the GNU General Public
0010: * License as published by the Free Software Foundation;
0011: * either version 2 of the License, or (at your option) any
0012: * later version.
0013: *
0014: * The GNU General Public License can be found at
0015: * http://www.gnu.org/copyleft/gpl.html.
0016: * A copy is found in the textfile GPL.txt and important notices to the
0017: * license from the author are found in LICENSE.txt distributed with
0018: * these libraries.
0019: *
0020: * This library is distributed in the hope that it will be useful,
0021: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0022: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0023: * GNU General Public License for more details.
0024: *
0025: * For further information about UCS - unique computing solutions gmbh,
0026: * please see the company website: http://www.ucs.at
0027: *
0028: * For further information about [fleXive](R), please see the
0029: * project website: http://www.flexive.org
0030: *
0031: *
0032: * This copyright notice MUST APPEAR in all copies of the file!
0033: ***************************************************************/package com.flexive.shared.value;
0034:
0035: import com.flexive.shared.*;
0036: import com.flexive.shared.exceptions.FxInvalidParameterException;
0037: import com.flexive.shared.exceptions.FxInvalidStateException;
0038: import com.flexive.shared.exceptions.FxNoAccessException;
0039: import com.flexive.shared.security.UserTicket;
0040: import com.flexive.shared.value.renderer.FxValueRendererFactory;
0041: import org.apache.commons.lang.ArrayUtils;
0042:
0043: import java.io.Serializable;
0044: import java.lang.reflect.Method;
0045: import java.util.HashMap;
0046: import java.util.Map;
0047: import java.util.Map.Entry;
0048:
0049: /**
0050: * Abstract base class of all value objects.
0051: * Common base classed is used for multilingual properties, etc.
0052: * <p/>
0053: * To check if a value is empty a flag is used for each language resp. the single value.
0054: * Use the setEmpty() method to explicity set a value to be empty
0055: *
0056: * @author Markus Plesser (markus.plesser@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
0057: */
0058: public abstract class FxValue<T, TDerived extends FxValue<T, TDerived>>
0059: implements Serializable, Comparable<FxValue> {
0060: private static final long serialVersionUID = -5005063788615664383L;
0061:
0062: public static final boolean DEFAULT_MULTILANGUAGE = true;
0063: private boolean multiLanguage;
0064: private long defaultLanguage = FxLanguage.SYSTEM_ID;
0065: private long selectedLanguage;
0066: private int maxInputLength;
0067: private String XPath = "";
0068:
0069: private final static Long[] SYSTEM_LANG_ARRAY = new Long[] { FxLanguage.SYSTEM_ID };
0070:
0071: /**
0072: * Data if <code>multiLanguage</code> is enabled
0073: */
0074: protected Map<Long, T> translations;
0075: protected Map<Long, Boolean> emptyTranslations;
0076:
0077: /**
0078: * Data if <code>multiLanguage</code> is disabled
0079: */
0080: protected T singleValue;
0081: private boolean singleValueEmpty;
0082: private boolean readOnly;
0083:
0084: protected FxValue() {
0085:
0086: }
0087:
0088: /**
0089: * Constructor
0090: *
0091: * @param multiLanguage multilanguage value?
0092: * @param defaultLanguage the default language
0093: * @param translations HashMap containing language->translation mapping
0094: */
0095: protected FxValue(boolean multiLanguage, long defaultLanguage,
0096: Map<Long, T> translations) {
0097: this .defaultLanguage = defaultLanguage;
0098: this .multiLanguage = multiLanguage;
0099: this .maxInputLength = -1;
0100: this .readOnly = false;
0101: if (multiLanguage) {
0102: if (translations == null) {
0103: //valid to pass null, create an empty one
0104: this .translations = new HashMap<Long, T>(5);
0105: this .emptyTranslations = new HashMap<Long, Boolean>(5);
0106: } else {
0107: this .translations = new HashMap<Long, T>(translations);
0108: this .emptyTranslations = new HashMap<Long, Boolean>(
0109: translations.size());
0110: }
0111: if (this .defaultLanguage < 0) {
0112: this .defaultLanguage = FxLanguage.SYSTEM_ID;
0113: this .selectedLanguage = FxLanguage.SYSTEM_ID;
0114: if (translations != null
0115: && !translations.entrySet().isEmpty())
0116: for (Entry<Long, T> e : translations.entrySet())
0117: if (e.getValue() != null) {
0118: this .selectedLanguage = e.getKey();
0119: break;
0120: }
0121: } else
0122: this .selectedLanguage = this .defaultLanguage;
0123: for (Long lang : this .translations.keySet())
0124: emptyTranslations.put(lang,
0125: this .translations.get(lang) == null);
0126: } else {
0127: if (translations != null && !translations.isEmpty()) {
0128: //a translation is provided, use the defaultLanguage element or very first element if not present
0129: singleValue = translations.get(defaultLanguage);
0130: if (singleValue == null)
0131: singleValue = translations.values().iterator()
0132: .next();
0133: }
0134: this .defaultLanguage = FxLanguage.SYSTEM_ID;
0135: this .selectedLanguage = FxLanguage.SYSTEM_ID;
0136: this .translations = null;
0137: this .singleValueEmpty = false;
0138: }
0139: }
0140:
0141: /**
0142: * Constructor
0143: *
0144: * @param defaultLanguage the default language
0145: * @param translations HashMap containing language->translation mapping
0146: */
0147: protected FxValue(long defaultLanguage, Map<Long, T> translations) {
0148: this (DEFAULT_MULTILANGUAGE, defaultLanguage, translations);
0149: }
0150:
0151: /**
0152: * Constructor
0153: *
0154: * @param multiLanguage multilanguage value?
0155: * @param translations HashMap containing language->translation mapping
0156: */
0157: protected FxValue(boolean multiLanguage, Map<Long, T> translations) {
0158: this (multiLanguage, FxLanguage.SYSTEM_ID, translations);
0159: }
0160:
0161: /**
0162: * Constructor
0163: *
0164: * @param translations HashMap containing language->translation mapping
0165: */
0166: protected FxValue(Map<Long, T> translations) {
0167: this (DEFAULT_MULTILANGUAGE, FxLanguage.SYSTEM_ID, translations);
0168: }
0169:
0170: /**
0171: * Constructor - create value from an array of translations
0172: *
0173: * @param translations HashMap containing language->translation mapping
0174: * @param pos position (index) in the array to use
0175: */
0176: protected FxValue(Map<Long, T[]> translations, int pos) {
0177: this (DEFAULT_MULTILANGUAGE, FxLanguage.SYSTEM_ID,
0178: new HashMap<Long, T>((translations == null ? 5
0179: : translations.size())));
0180: if (multiLanguage) {
0181: if (translations == null)
0182: return;
0183: for (Entry<Long, T[]> e : translations.entrySet())
0184: if (e.getValue()[pos] != null)
0185: this .translations
0186: .put(e.getKey(), e.getValue()[pos]);
0187: } else
0188: this .singleValue = (translations == null
0189: || translations.isEmpty() ? null : translations
0190: .values().iterator().next()[pos]);
0191: }
0192:
0193: /**
0194: * Constructor
0195: *
0196: * @param multiLanguage multilanguage value?
0197: * @param defaultLanguage the default language
0198: * @param value single initializing value
0199: */
0200: protected FxValue(boolean multiLanguage, long defaultLanguage,
0201: T value) {
0202: this (multiLanguage, defaultLanguage, new HashMap<Long, T>(5));
0203: if (multiLanguage)
0204: this .translations.put(defaultLanguage, value);
0205: else
0206: this .singleValue = value;
0207: }
0208:
0209: /**
0210: * Constructor
0211: *
0212: * @param defaultLanguage the default language
0213: * @param value single initializing value
0214: */
0215: protected FxValue(long defaultLanguage, T value) {
0216: this (DEFAULT_MULTILANGUAGE, defaultLanguage, value);
0217: }
0218:
0219: /**
0220: * Constructor
0221: *
0222: * @param multiLanguage multilanguage value?
0223: * @param value single initializing value
0224: */
0225: protected FxValue(boolean multiLanguage, T value) {
0226: this (multiLanguage, FxLanguage.DEFAULT_ID, value);
0227: }
0228:
0229: /**
0230: * Constructor
0231: *
0232: * @param value single initializing value
0233: */
0234: protected FxValue(T value) {
0235: this (DEFAULT_MULTILANGUAGE, value);
0236: }
0237:
0238: /**
0239: * Constructor
0240: *
0241: * @param clone original FxValue to be cloned
0242: */
0243: @SuppressWarnings("unchecked")
0244: protected FxValue(FxValue<T, TDerived> clone) {
0245: this (
0246: clone.isMultiLanguage(),
0247: clone.getDefaultLanguage(),
0248: new HashMap<Long, T>(
0249: (clone.translations != null ? clone.translations
0250: .size()
0251: : 1)));
0252: this .XPath = clone.XPath;
0253: if (clone.isImmutableValueType()) {
0254: if (multiLanguage) {
0255: // clone only hashmap
0256: this .translations = new HashMap(clone.translations);
0257: this .emptyTranslations = new HashMap(
0258: clone.emptyTranslations);
0259: } else {
0260: this .singleValue = clone.singleValue;
0261: this .singleValueEmpty = clone.singleValueEmpty;
0262: }
0263: } else {
0264: if (multiLanguage) {
0265: // clone hashmap values
0266: Method meth = null;
0267: for (long k : clone.translations.keySet()) {
0268: T t = clone.translations.get(k);
0269: if (t == null)
0270: this .translations.put(k, null);
0271: else {
0272: try {
0273: if (meth != null) {
0274: this .translations.put(k, (T) meth
0275: .invoke(t));
0276: } else {
0277: Class<?> clzz = t.getClass();
0278: meth = clzz.getMethod("clone");
0279: }
0280: } catch (Exception e) {
0281: throw new IllegalArgumentException(
0282: "clone not supported", e);
0283: }
0284: }
0285: }
0286: this .emptyTranslations = new HashMap(
0287: clone.emptyTranslations);
0288: } else {
0289: try {
0290: this .singleValue = (T) clone.singleValue.getClass()
0291: .getMethod("clone").invoke(
0292: clone.singleValue);
0293: } catch (Exception e) {
0294: throw new IllegalArgumentException(
0295: "clone not supported", e);
0296: }
0297: this .singleValueEmpty = clone.singleValueEmpty;
0298: }
0299: }
0300: }
0301:
0302: /**
0303: * Get the XPath for this value - the XPath is optional and can be an empty String if
0304: * not explicitly assigned!
0305: *
0306: * @return XPath (optional! can be an empty String)
0307: */
0308: public String getXPath() {
0309: return XPath;
0310: }
0311:
0312: /**
0313: * Returns the name of the value from the xpath.
0314: * <p/>
0315: * If the xpath is an empty string the name will also return an emptry String.
0316: *
0317: * @return the property name
0318: */
0319: public String getXPathName() {
0320: try {
0321: String xpathSplit[] = getXPath().split("/");
0322: return xpathSplit[xpathSplit.length - 1].split("\\[")[0];
0323: } catch (Throwable t) {
0324: return "";
0325: }
0326: }
0327:
0328: /**
0329: * Set the XPath (unless readonly)
0330: *
0331: * @param XPath the XPath to set, will be ignored if readonly
0332: * @return this
0333: */
0334: @SuppressWarnings({"unchecked"})
0335: public TDerived setXPath(String XPath) {
0336: if (!this .readOnly) {
0337: this .XPath = XPath;
0338: }
0339: return (TDerived) this ;
0340: }
0341:
0342: /**
0343: * One-time operation to flag this FxValue as read only.
0344: * This is not reversible!
0345: */
0346: public void setReadOnly() {
0347: this .readOnly = true;
0348: }
0349:
0350: /**
0351: * Mark this FxValue as empty
0352: *
0353: * @return this
0354: */
0355: @SuppressWarnings("unchecked")
0356: public TDerived setEmpty() {
0357: if (this .multiLanguage) {
0358: if (this .emptyTranslations == null)
0359: this .emptyTranslations = new HashMap<Long, Boolean>(
0360: this .translations.size());
0361: for (Long lang : this .translations.keySet())
0362: this .emptyTranslations.put(lang, true);
0363: } else
0364: this .singleValueEmpty = true;
0365: return (TDerived) this ;
0366: }
0367:
0368: /**
0369: * Mark the entry for the given language as empty
0370: *
0371: * @param language the language to flag as empty
0372: */
0373: public void setEmpty(long language) {
0374: if (this .multiLanguage) {
0375: if (this .emptyTranslations == null)
0376: this .emptyTranslations = new HashMap<Long, Boolean>(
0377: this .translations.size());
0378: this .emptyTranslations.put(language, true);
0379: } else
0380: this .singleValueEmpty = true;
0381: }
0382:
0383: /**
0384: * Return the class instance of the value type.
0385: *
0386: * @return the class instance of the value type.
0387: */
0388: public abstract Class<T> getValueClass();
0389:
0390: /**
0391: * Evaluates the given string value to an object of type T.
0392: *
0393: * @param value string value to be evaluated
0394: * @return the value interpreted as T
0395: */
0396: public abstract T fromString(String value);
0397:
0398: /**
0399: * Converts the given instance of T to a string that can be
0400: * parsed again by {@link FxValue#fromString(String)}.
0401: *
0402: * @param value the value to be converted
0403: * @return a string representation of the given value that can be parsed again using
0404: * {@link FxValue#fromString(String)}.
0405: */
0406: public String getStringValue(T value) {
0407: return String.valueOf(value);
0408: }
0409:
0410: /**
0411: * Creates a copy of the given object (useful if the actual type is unknown).
0412: *
0413: * @return a copy of the given object (useful if the actual type is unknown).
0414: */
0415: public abstract TDerived copy();
0416:
0417: /**
0418: * Return true if T is immutable (e.g. java.lang.String). This prevents cloning
0419: * of the translations in copy constructors.
0420: *
0421: * @return true if T is immutable (e.g. java.lang.String)
0422: */
0423: public boolean isImmutableValueType() {
0424: return true;
0425: }
0426:
0427: /**
0428: * Is this value editable by the user?
0429: * This always returns true except it is a FxNoAccess value or flagged as readOnly
0430: *
0431: * @return if this value editable?
0432: * @see FxNoAccess
0433: */
0434: public boolean isReadOnly() {
0435: return readOnly;
0436: }
0437:
0438: /**
0439: * Returns true if this value is valid for the actual type (e.g. if
0440: * a FxNumber property actually contains only valid numbers).
0441: *
0442: * @return true if this value is valid for the actual type
0443: */
0444: public boolean isValid() {
0445: //noinspection UnusedCatchParameter
0446: try {
0447: getErrorValue();
0448: // an error value exists, thus this instance is invalid
0449: return false;
0450: } catch (IllegalStateException e) {
0451: // this instance is valid, thus no error value could be retrieved
0452: return true;
0453: }
0454: }
0455:
0456: /**
0457: * Returns the value that caused {@link #isValid} to return false. If isValid() is true,
0458: * a RuntimeException is thrown.
0459: *
0460: * @return the value that caused the validation via {@link #isValid} to fail
0461: * @throws IllegalStateException if the instance is valid and the error value is undefined
0462: */
0463: @SuppressWarnings({"UnusedCatchParameter"})
0464: public T getErrorValue() throws IllegalStateException {
0465: if (multiLanguage) {
0466: for (T translation : translations.values()) {
0467: if (translation instanceof String) {
0468: // if a string was used, check if it is a valid representation of our type
0469: try {
0470: fromString((String) translation);
0471: } catch (Exception e) {
0472: return translation;
0473: }
0474: }
0475: }
0476: } else if (singleValue instanceof String) {
0477: try {
0478: fromString((String) singleValue);
0479: } catch (Exception e) {
0480: return singleValue;
0481: }
0482: }
0483: throw new IllegalStateException();
0484: }
0485:
0486: /**
0487: * Get a representation of this value in the default translation
0488: *
0489: * @return T
0490: */
0491: public T getDefaultTranslation() {
0492: if (!multiLanguage)
0493: return singleValue;
0494: T def = getTranslation(getDefaultLanguage());
0495: if (def != null)
0496: return def;
0497: if (translations.size() > 0)
0498: return translations.values().iterator().next(); //first available translation if default does not exist
0499: return def; //empty as last fallback
0500: }
0501:
0502: /**
0503: * Get the translation for a requested language
0504: *
0505: * @param lang requested language
0506: * @return translation or an empty String if it does not exist
0507: */
0508: public T getTranslation(long lang) {
0509: return (multiLanguage ? translations.get(lang) : singleValue);
0510: }
0511:
0512: /**
0513: * Get a String representation of this value in the requested language or
0514: * an empty String if the translation does not exist
0515: *
0516: * @param lang requested language id
0517: * @return T translation
0518: */
0519: public T getTranslation(FxLanguage lang) {
0520: if (!multiLanguage) //redundant but faster
0521: return singleValue;
0522: return getTranslation((int) lang.getId());
0523: }
0524:
0525: /**
0526: * Get the translation that best fits the requested language.
0527: * The requested language is queried and if it does not exist the
0528: * default translation is returned
0529: *
0530: * @param lang requested best-fit language
0531: * @return best fit translation
0532: */
0533: public T getBestTranslation(long lang) {
0534: if (!multiLanguage) //redundant but faster
0535: return singleValue;
0536: T ret = getTranslation(lang);
0537: if (ret != null)
0538: return ret;
0539: return getDefaultTranslation();
0540: }
0541:
0542: /**
0543: * Get the translation that best fits the requested language.
0544: * The requested language is queried and if it does not exist the
0545: * default translation is returned
0546: *
0547: * @param language requested best-fit language
0548: * @return best fit translation
0549: */
0550: public T getBestTranslation(FxLanguage language) {
0551: if (!multiLanguage) //redundant but faster
0552: return singleValue;
0553: if (language == null) // user ticket language
0554: return getBestTranslation();
0555: return getBestTranslation((int) language.getId());
0556: }
0557:
0558: /**
0559: * Get the translation that best fits the requested users language.
0560: * The requested users language is queried and if it does not exist the
0561: * default translation is returned
0562: *
0563: * @param ticket UserTicket to obtain the users language
0564: * @return best fit translation
0565: */
0566: public T getBestTranslation(UserTicket ticket) {
0567: if (!multiLanguage) //redundant but faster
0568: return singleValue;
0569: return getBestTranslation((int) ticket.getLanguage().getId());
0570: }
0571:
0572: /**
0573: * Get the translation that best fits the current users language.
0574: * The user language is obtained from the FxContext thread local.
0575: *
0576: * @return best fit translation
0577: */
0578: public T getBestTranslation() {
0579: if (!multiLanguage) //redundant but faster
0580: return singleValue;
0581: return getBestTranslation(FxContext.get().getTicket()
0582: .getLanguage());
0583: }
0584:
0585: /**
0586: * Get all languages for which translations exist
0587: *
0588: * @return languages for which translations exist
0589: */
0590: public Long[] getTranslatedLanguages() {
0591: return (multiLanguage ? translations.keySet().toArray(
0592: new Long[translations.keySet().size()])
0593: : SYSTEM_LANG_ARRAY.clone());
0594: }
0595:
0596: /**
0597: * Does a translation exist for the given language?
0598: *
0599: * @param languageId language to query
0600: * @return translation exists
0601: */
0602: public boolean translationExists(long languageId) {
0603: return !multiLanguage || translations.get(languageId) != null;
0604: }
0605:
0606: /**
0607: * Like empty(), for JSF EL, since empty cannot be used.
0608: *
0609: * @return true if the value is empty
0610: */
0611: public boolean getIsEmpty() {
0612: return isEmpty();
0613: }
0614:
0615: /**
0616: * Is this value empty?
0617: *
0618: * @return if value is empty
0619: */
0620: public boolean isEmpty() {
0621: if (multiLanguage) {
0622: syncEmptyTranslations();
0623: for (boolean check : emptyTranslations.values())
0624: if (!check)
0625: return false;
0626: return true;
0627: } else
0628: return singleValueEmpty;
0629: }
0630:
0631: /**
0632: * Check if the translation for the given language is empty
0633: *
0634: * @param lang language to check
0635: * @return if translation for the given language is empty
0636: */
0637: public boolean isTranslationEmpty(FxLanguage lang) {
0638: return lang != null ? isTranslationEmpty(lang.getId())
0639: : isEmpty();
0640: }
0641:
0642: /**
0643: * Check if the translation for the given language is empty
0644: *
0645: * @param lang language to check
0646: * @return if translation for the given language is empty
0647: */
0648: public boolean isTranslationEmpty(long lang) {
0649: if (!multiLanguage)
0650: return singleValueEmpty;
0651: syncEmptyTranslations();
0652: return !emptyTranslations.containsKey(lang)
0653: || emptyTranslations.get(lang);
0654: }
0655:
0656: /**
0657: * Synchronize - and create if needed - empty tanslations
0658: */
0659: private void syncEmptyTranslations() {
0660: if (!multiLanguage)
0661: return;
0662: if (emptyTranslations == null)
0663: emptyTranslations = new HashMap<Long, Boolean>(translations
0664: .size());
0665: if (emptyTranslations.size() != translations.size()) {
0666: //resync
0667: for (Long _lang : translations.keySet()) {
0668: if (!emptyTranslations.containsKey(_lang))
0669: emptyTranslations.put(_lang, translations
0670: .get(_lang) == null);
0671: }
0672: }
0673: }
0674:
0675: /**
0676: * Get the language selected in user interfaces
0677: *
0678: * @return selected language
0679: */
0680: public long getSelectedLanguage() {
0681: return selectedLanguage;
0682: }
0683:
0684: /**
0685: * Set the user selected language
0686: *
0687: * @param selectedLanguage selected language ID
0688: * @return self
0689: * @throws FxNoAccessException if the selected Language is not contained
0690: */
0691: public FxValue setSelectedLanguage(long selectedLanguage)
0692: throws FxNoAccessException {
0693: if (selectedLanguage < 0 || !multiLanguage)
0694: return this ;
0695: //throw exception if selectedLanguage is not contained!
0696: if (!translations.containsKey(selectedLanguage))
0697: throw new FxNoAccessException(
0698: "ex.content.value.invalid.language",
0699: selectedLanguage);
0700: this .selectedLanguage = selectedLanguage;
0701: return this ;
0702: }
0703:
0704: /**
0705: * Set the translation for a language or override the single language value if
0706: * this value is not flagged as multi language enabled. This method cannot be
0707: * overridden since it not only accepts parameters of type T, but also of type
0708: * String for web form handling.
0709: *
0710: * @param language language to set the translation for
0711: * @param value translation
0712: * @return this
0713: */
0714: public final TDerived setTranslation(long language, T value) {
0715: if (value instanceof String) {
0716: try {
0717: value = this .fromString((String) value);
0718: } catch (Exception e) {
0719: // do nothing. The resulting FxValue will be invalid,
0720: // but the invalid value will be preserved.
0721: // TODO: use a "safer" concept of representing invalid translations,
0722: // since this may lead to unexpeced ClassCastExceptions in parameterized
0723: // methods expecting a <T> value
0724: }
0725: }
0726: if (!multiLanguage) {
0727: if (value == null && !isAcceptsEmptyDefaultTranslations()) {
0728: throw new FxInvalidParameterException("value",
0729: "ex.content.invalid.default.empty", getClass()
0730: .getSimpleName()).asRuntimeException();
0731: }
0732: //override the single value
0733: if (singleValue == null || !singleValue.equals(value))
0734: this .singleValue = value;
0735: this .singleValueEmpty = value == null;
0736: //noinspection unchecked
0737: return (TDerived) this ;
0738: }
0739: if (language == FxLanguage.SYSTEM_ID)
0740: throw new FxInvalidParameterException("language",
0741: "ex.content.value.invalid.multilanguage.sys")
0742: .asRuntimeException();
0743: if (value == null) {
0744: translations.remove(language);
0745: emptyTranslations.remove(language);
0746: } else {
0747: if (!value.equals(translations.get(language))) {
0748: translations.put(language, value);
0749: }
0750: emptyTranslations.put(language, false);
0751: }
0752: //noinspection unchecked
0753: return (TDerived) this ;
0754: }
0755:
0756: /**
0757: * Set the translation for a language or override the single language value if
0758: * this value is not flagged as multi language enabled
0759: *
0760: * @param lang language to set the translation for
0761: * @param translation translation
0762: * @return this
0763: */
0764: public TDerived setTranslation(FxLanguage lang, T translation) {
0765: return setTranslation((int) lang.getId(), translation);
0766: }
0767:
0768: /**
0769: * For multilanguage values, set the default translation.
0770: * For single language values, set the value.
0771: *
0772: * @param value the value to be stored
0773: */
0774: public void setValue(T value) {
0775: setTranslation(getDefaultLanguage(), value);
0776: }
0777:
0778: /**
0779: * Set the translation in the default language. For single-language values,
0780: * sets the value.
0781: *
0782: * @param translation the default translation
0783: * @return this
0784: */
0785: public FxValue setDefaultTranslation(T translation) {
0786: return setTranslation(defaultLanguage, translation);
0787: }
0788:
0789: /**
0790: * Get the default language of this value
0791: *
0792: * @return default language
0793: */
0794: public long getDefaultLanguage() {
0795: if (!isMultiLanguage())
0796: return FxLanguage.SYSTEM_ID;
0797: return this .defaultLanguage;
0798: }
0799:
0800: /**
0801: * Returns the maximum input length an input field should have for this value
0802: * (or -1 for unlimited length).
0803: *
0804: * @return the maximum input length an input field should have for this value
0805: */
0806: public int getMaxInputLength() {
0807: return maxInputLength;
0808: }
0809:
0810: /**
0811: * Set the maximum input length for this value (-1 for unlimited length).
0812: *
0813: * @param maxInputLength the maximum input length for this value (-1 for unlimited length).
0814: */
0815: public void setMaxInputLength(int maxInputLength) {
0816: this .maxInputLength = maxInputLength;
0817: }
0818:
0819: /**
0820: * Set the default language.
0821: * It will only be set if a translation in the requested default language
0822: * exists!
0823: *
0824: * @param defaultLanguage requested default language
0825: */
0826: public void setDefaultLanguage(long defaultLanguage) {
0827: setDefaultLanguage(defaultLanguage, false);
0828: }
0829:
0830: /**
0831: * Set the default language. Will have no effect if the value is not multi language enabled
0832: *
0833: * @param defaultLanguage requested default language
0834: * @param force if true, the default language will also be updated if no translation exists (for UI input)
0835: */
0836: public void setDefaultLanguage(long defaultLanguage, boolean force) {
0837: if (multiLanguage
0838: && (force || translationExists(defaultLanguage))) {
0839: this .defaultLanguage = defaultLanguage;
0840: }
0841: }
0842:
0843: /**
0844: * Reset the default language to the system language
0845: */
0846: public void clearDefaultLanguage() {
0847: this .defaultLanguage = FxLanguage.SYSTEM_ID;
0848: }
0849:
0850: /**
0851: * Is a default value set for this FxValue?
0852: *
0853: * @return default value set
0854: */
0855: public boolean hasDefaultLanguage() {
0856: return defaultLanguage != FxLanguage.SYSTEM_ID
0857: && isMultiLanguage();
0858: }
0859:
0860: /**
0861: * Check if the passed language is the default language
0862: *
0863: * @param language the language to check
0864: * @return passed language is the default language
0865: */
0866: public boolean isDefaultLanguage(long language) {
0867: return hasDefaultLanguage() && language == defaultLanguage;
0868: }
0869:
0870: /**
0871: * Remove the translation for the given language
0872: *
0873: * @param language the language to remove the translation for
0874: */
0875: public void removeLanguage(long language) {
0876: if (!multiLanguage) {
0877: setEmpty();
0878: } else {
0879: translations.remove(language);
0880: emptyTranslations.remove(language);
0881: }
0882: }
0883:
0884: /**
0885: * Is this value available for multiple languages?
0886: *
0887: * @return value available for multiple languages
0888: */
0889: public boolean isMultiLanguage() {
0890: return this .multiLanguage;
0891: }
0892:
0893: protected boolean isAcceptsEmptyDefaultTranslations() {
0894: return true;
0895: }
0896:
0897: /**
0898: * Format this FxValue for inclusion in a SQL statement. For example,
0899: * a string is wrapped in single quotes and escaped properly (' --> '').
0900: * For multilanguage values the default translation is used. If the value is
0901: * empty (@link #isEmpty()), a runtime exception is thrown.
0902: *
0903: * @return the formatted value
0904: */
0905: public String getSqlValue() {
0906: if (isEmpty()) {
0907: throw new FxInvalidStateException(
0908: "ex.content.value.sql.empty").asRuntimeException();
0909: }
0910: return FxFormatUtils.escapeForSql(getDefaultTranslation());
0911: }
0912:
0913: /**
0914: * {@inheritDoc}
0915: */
0916: @Override
0917: public String toString() {
0918: // format value in the current user's locale - also used in the JSF UI
0919: return FxValueRendererFactory.getInstance().format(this );
0920: /*if (isEmpty()) {
0921: return "";
0922: }
0923: if (!multiLanguage)
0924: return singleValue == null ? "" : singleValue.toString();
0925:
0926: String ret = null;
0927: try {
0928: if (translations != null && translations.size() > 0) {
0929: final UserTicket ticket = FxContext.get().getUserTicket();
0930: if (ticket != null) {
0931: ret = getBestTranslation(ticket.getLanguage().getId()).toString();
0932: } else {
0933: ret = getTranslation(FxLanguage.DEFAULT_ID).toString();
0934: if (ret == null)
0935: ret = translations.values().iterator().next().toString();
0936: }
0937: }
0938: } catch (Exception e) {
0939: ret = null;
0940: }
0941: return ret != null ? ret : "";*/
0942: }
0943:
0944: /**
0945: * {@inheritDoc}
0946: */
0947: @Override
0948: public boolean equals(Object other) {
0949: if (this == other)
0950: return true;
0951: if (other == null)
0952: return false;
0953: if (this .getClass() != other.getClass())
0954: return false;
0955: FxValue<?, ?> otherValue = (FxValue<?, ?>) other;
0956: if (this .isEmpty() != otherValue.isEmpty())
0957: return false;
0958: if (this .isMultiLanguage() != otherValue.isMultiLanguage())
0959: return false;
0960: if (multiLanguage) {
0961: if (!ArrayUtils.isEquals(
0962: this .translations.keySet()
0963: .toArray(
0964: new Long[this .translations.keySet()
0965: .size()]),
0966: otherValue.translations.keySet().toArray(
0967: new Long[otherValue.translations.keySet()
0968: .size()])))
0969: return false;
0970: if (!ArrayUtils.isEquals(this .translations.values()
0971: .toArray(), otherValue.translations.values()
0972: .toArray()))
0973: return false;
0974: } else {
0975: if (!this .isEmpty())
0976: if (!this .singleValue.equals(otherValue.singleValue))
0977: return false;
0978: }
0979: return true;
0980: }
0981:
0982: /**
0983: * {@inheritDoc}
0984: */
0985: @Override
0986: public int hashCode() {
0987: int hash = 7;
0988: for (T translation : translations.values()) {
0989: hash = 31 * hash + translation.hashCode();
0990: }
0991: hash = 31 * hash + (int) defaultLanguage;
0992: for (long language : translations.keySet()) {
0993: hash = 31 * hash + (int) language;
0994: }
0995: return hash;
0996: }
0997:
0998: /**
0999: * Convert to XML
1000: *
1001: * @return xml
1002: */
1003: public String toXML() {
1004: StringBuilder sb = new StringBuilder(500);
1005: sb.append("<").append(getClass().getName()).append(" m=\"")
1006: .append(isMultiLanguage() ? "1" : "0").append("\"");
1007: if (hasDefaultLanguage())
1008: sb.append(" d=\"").append(getDefaultLanguage())
1009: .append("\"");
1010: sb.append(">");
1011: if (!isMultiLanguage())
1012: sb.append(FxXMLUtils.toCData(getStringValue(singleValue)));
1013: else
1014: for (long i : getTranslatedLanguages())
1015: sb
1016: .append("<data l=\"")
1017: .append(i)
1018: .append("\">")
1019: .append(
1020: FxXMLUtils
1021: .toCData(getStringValue(getTranslation(i))))
1022: .append("</data>");
1023: sb.append("</").append(getClass().getName()).append(">");
1024: return sb.toString();
1025: }
1026:
1027: /**
1028: * Get an FxValue from its XML representation
1029: *
1030: * @param xml XML representation
1031: * @return FxValue instance
1032: */
1033: public static FxValue fromXML(String xml) {
1034: String clazz = xml.substring(1, xml.indexOf(' '));
1035: try {
1036: FxValue v = (FxValue) Class.forName(
1037: "com.flexive.shared.content.values." + clazz)
1038: .newInstance();
1039: v.multiLanguage = xml.indexOf("m=\"1\"") > 0;
1040: int def;
1041: if ((def = xml.indexOf("d=\"")) > 0)
1042: v.defaultLanguage = Long.parseLong(xml.substring(
1043: def + 2, xml.indexOf("\"", def + 2)));
1044:
1045: if (v.multiLanguage) {
1046: StringBuilder data = new StringBuilder(xml.substring(
1047: xml.indexOf('>'), xml.lastIndexOf("</" + clazz
1048: + ">")));
1049: int start = 0;
1050: while ((start = data.indexOf("<data", start)) >= 0) {
1051: v.setTranslation(Long.parseLong(data.substring(data
1052: .indexOf("l=\"", start + 3), data
1053: .indexOf("\">"))), v.fromString(FxXMLUtils
1054: .fromCData(data.substring(data.indexOf(">",
1055: start), data.indexOf("</data>",
1056: start)))));
1057: }
1058: } else {
1059: v.singleValue = v.fromString(xml.substring(xml
1060: .indexOf(">"), xml.lastIndexOf("</")));
1061: }
1062: return v;
1063: } catch (Exception e) {
1064: e.printStackTrace();
1065: }
1066: return null;
1067: }
1068:
1069: /**
1070: * A generic comparable implementation based on the value's string representation.
1071: *
1072: * @param o the other object
1073: * @return see {@link Comparable#compareTo}.
1074: */
1075: @SuppressWarnings({"unchecked"})
1076: public int compareTo(FxValue o) {
1077: if (isEmpty() && !o.isEmpty()) {
1078: return -1;
1079: }
1080: if (isEmpty() && o.isEmpty()) {
1081: return 0;
1082: }
1083: if (!isEmpty() && o.isEmpty()) {
1084: return 1;
1085: }
1086: return FxSharedUtils.getCollator().compare(
1087: getStringValue(getBestTranslation()),
1088: o.getStringValue(o.getBestTranslation()));
1089: }
1090: }
|