com.xpn.xwiki.objects.classes.PropertyClass.java Source code

Java tutorial

Introduction

Here is the source code for com.xpn.xwiki.objects.classes.PropertyClass.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package com.xpn.xwiki.objects.classes;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.ecs.xhtml.input;
import org.apache.velocity.VelocityContext;
import org.dom4j.Element;
import org.dom4j.dom.DOMElement;
import org.hibernate.mapping.Property;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.model.reference.ClassPropertyReference;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.velocity.VelocityManager;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.internal.xml.XMLAttributeValueFilter;
import com.xpn.xwiki.objects.BaseCollection;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.BaseProperty;
import com.xpn.xwiki.objects.meta.MetaClass;
import com.xpn.xwiki.objects.meta.PropertyMetaClass;
import com.xpn.xwiki.validation.XWikiValidationStatus;
import com.xpn.xwiki.web.Utils;

/**
 * Represents an XClass property and contains property definitions (eg "relational storage", "display type",
 * "separator", "multi select", etc). Each property definition is of type {@link BaseProperty}.
 * 
 * @version $Id: edeb64a82db9f2631cefb5166276c176dd1ca40d $
 */
public class PropertyClass extends BaseCollection<ClassPropertyReference>
        implements PropertyClassInterface, Comparable<PropertyClass> {
    /**
     * Logging helper object.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(PropertyClass.class);

    /**
     * Identifier used to specify that the property has a custom displayer in the XClass itself.
     */
    private static final String CLASS_DISPLAYER_IDENTIFIER = "class";

    /**
     * Identifier prefix used to specify that the property has a custom displayer in a wiki document.
     */
    private static final String DOCUMENT_DISPLAYER_IDENTIFIER_PREFIX = "doc:";

    /**
     * Identifier prefix used to specify that the property has a custom displayer in a velocity template.
     */
    private static final String TEMPLATE_DISPLAYER_IDENTIFIER_PREFIX = "template:";

    private BaseClass xclass;

    private long id;

    private PropertyMetaClass pMetaClass;

    protected String cachedCustomDisplayer;

    public PropertyClass() {
    }

    public PropertyClass(String name, String prettyname, PropertyMetaClass xWikiClass) {
        setName(name);
        setPrettyName(prettyname);
        setxWikiClass(xWikiClass);
        setUnmodifiable(false);
        setDisabled(false);
    }

    @Override
    protected ClassPropertyReference createReference() {
        return new ClassPropertyReference(getName(), this.xclass.getReference());
    }

    @Override
    public BaseClass getXClass(XWikiContext context) {
        return getxWikiClass();
    }

    public BaseClass getxWikiClass() {
        if (this.pMetaClass == null) {
            MetaClass metaClass = MetaClass.getMetaClass();
            this.pMetaClass = (PropertyMetaClass) metaClass.get(getClassType());
        }
        return this.pMetaClass;
    }

    public void setxWikiClass(BaseClass xWikiClass) {
        this.pMetaClass = (PropertyMetaClass) xWikiClass;
    }

    @Override
    public BaseCollection getObject() {
        return this.xclass;
    }

    @Override
    public void setObject(BaseCollection object) {
        this.xclass = (BaseClass) object;
    }

    public String getFieldFullName() {
        if (getObject() == null) {
            return getName();
        }
        return getObject().getName() + "_" + getName();
    }

    @Override
    public long getId() {
        if (getObject() == null) {
            return this.id;
        }
        return getObject().getId();
    }

    @Override
    public void setId(long id) {
        this.id = id;
    }

    @Override
    public String toString(BaseProperty property) {
        return property.toText();
    }

    @Override
    public BaseProperty fromString(String value) {
        return null;
    }

    public BaseProperty newPropertyfromXML(Element ppcel) {
        String value = ppcel.getText();
        return fromString(value);
    }

    @Override
    public void displayHidden(StringBuffer buffer, String name, String prefix, BaseCollection object,
            XWikiContext context) {
        input input = new input();
        input.setAttributeFilter(new XMLAttributeValueFilter());
        BaseProperty prop = (BaseProperty) object.safeget(name);
        if (prop != null) {
            input.setValue(prop.toText());
        }

        input.setType("hidden");
        input.setName(prefix + name);
        input.setID(prefix + name);
        buffer.append(input.toString());
    }

    @Override
    public void displayView(StringBuffer buffer, String name, String prefix, BaseCollection object,
            XWikiContext context) {
        BaseProperty prop = (BaseProperty) object.safeget(name);
        if (prop != null) {
            buffer.append(prop.toText());
        }
    }

    @Override
    public void displayEdit(StringBuffer buffer, String name, String prefix, BaseCollection object,
            XWikiContext context) {
        input input = new input();
        input.setAttributeFilter(new XMLAttributeValueFilter());

        BaseProperty prop = (BaseProperty) object.safeget(name);
        if (prop != null) {
            input.setValue(prop.toText());
        }

        input.setType("text");
        input.setName(prefix + name);
        input.setID(prefix + name);
        input.setDisabled(isDisabled());
        buffer.append(input.toString());
    }

    public String displayHidden(String name, String prefix, BaseCollection object, XWikiContext context) {
        StringBuffer buffer = new StringBuffer();
        displayHidden(buffer, name, prefix, object, context);
        return buffer.toString();
    }

    public String displayHidden(String name, BaseCollection object, XWikiContext context) {
        return displayHidden(name, "", object, context);
    }

    public String displayView(String name, String prefix, BaseCollection object, XWikiContext context) {
        StringBuffer buffer = new StringBuffer();
        displayView(buffer, name, prefix, object, context);
        return buffer.toString();
    }

    public String displayView(String name, BaseCollection object, XWikiContext context) {
        return displayView(name, "", object, context);
    }

    public String displayEdit(String name, String prefix, BaseCollection object, XWikiContext context) {
        StringBuffer buffer = new StringBuffer();
        displayEdit(buffer, name, prefix, object, context);
        return buffer.toString();
    }

    public String displayEdit(String name, BaseCollection object, XWikiContext context) {
        return displayEdit(name, "", object, context);
    }

    public boolean isCustomDisplayed(XWikiContext context) {
        return (StringUtils.isNotEmpty(getCachedDefaultCustomDisplayer(context)));
    }

    public void displayCustom(StringBuffer buffer, String fieldName, String prefix, String type, BaseObject object,
            XWikiContext context) throws XWikiException {
        String content = "";
        try {
            VelocityContext vcontext = Utils.getComponent(VelocityManager.class).getVelocityContext();
            vcontext.put("name", fieldName);
            vcontext.put("prefix", prefix);
            // The PropertyClass instance can be used to access meta properties in the custom displayer (e.g.
            // dateFormat, multiSelect). It can be obtained from the XClass of the given object but only if the property
            // has been added to the XClass. We need to have it in the Velocity context for the use case when an XClass
            // property needs to be previewed before being added to the XClass.
            vcontext.put("field", new com.xpn.xwiki.api.PropertyClass(this, context));
            vcontext.put("object", new com.xpn.xwiki.api.Object(object, context));
            vcontext.put("type", type);
            vcontext.put("context", new com.xpn.xwiki.api.Context(context));

            BaseProperty prop = (BaseProperty) object.safeget(fieldName);
            if (prop != null) {
                vcontext.put("value", prop.getValue());
            } else {
                // The $value property can exist in the velocity context, we overwrite it to make sure we don't get a
                // wrong value in the displayer when the property does not exist yet.
                vcontext.put("value", null);
            }

            String customDisplayer = getCachedDefaultCustomDisplayer(context);
            if (StringUtils.isNotEmpty(customDisplayer)) {
                if (customDisplayer.equals(CLASS_DISPLAYER_IDENTIFIER)) {
                    content = getCustomDisplay();
                    String classSyntax = context.getWiki().getDocument(getObject().getDocumentReference(), context)
                            .getSyntax().toIdString();
                    content = context.getDoc().getRenderedContent(content, classSyntax, context);
                } else if (customDisplayer.startsWith(DOCUMENT_DISPLAYER_IDENTIFIER_PREFIX)) {
                    XWikiDocument displayerDoc = context.getWiki().getDocument(
                            StringUtils.substringAfter(customDisplayer, DOCUMENT_DISPLAYER_IDENTIFIER_PREFIX),
                            context);
                    content = displayerDoc.getContent();
                    String classSyntax = displayerDoc.getSyntax().toIdString();
                    content = context.getDoc().getRenderedContent(content, classSyntax, context);
                } else if (customDisplayer.startsWith(TEMPLATE_DISPLAYER_IDENTIFIER_PREFIX)) {
                    content = context.getWiki().evaluateTemplate(
                            StringUtils.substringAfter(customDisplayer, TEMPLATE_DISPLAYER_IDENTIFIER_PREFIX),
                            context);
                }
            }
        } catch (Exception e) {
            throw new XWikiException(XWikiException.MODULE_XWIKI_CLASSES,
                    XWikiException.ERROR_XWIKI_CLASSES_CANNOT_PREPARE_CUSTOM_DISPLAY,
                    "Exception while preparing the custom display of " + fieldName, e, null);

        }
        buffer.append(content);
    }

    @Override
    public String getClassName() {
        BaseClass bclass = getxWikiClass();
        return (bclass == null) ? "" : bclass.getName();
    }

    // In property classes we need to store this info in the HashMap for fields
    // This way it is readable by the displayEdit/displayView functions..
    @Override
    public String getName() {
        return getStringValue("name");
    }

    @Override
    public void setName(String name) {
        setStringValue("name", name);
    }

    public String getCustomDisplay() {
        return getStringValue("customDisplay");
    }

    public void setCustomDisplay(String value) {
        setLargeStringValue("customDisplay", value);
    }

    @Override
    public String getPrettyName() {
        return getStringValue("prettyName");
    }

    public String getPrettyName(XWikiContext context) {
        return getTranslatedPrettyName(context);
    }

    public String getTranslatedPrettyName(XWikiContext context) {
        String msgName = getFieldFullName();
        if ((context == null) || (context.getWiki() == null)) {
            return getPrettyName();
        }

        String prettyName = context.getMessageTool().get(msgName);
        if (prettyName.equals(msgName)) {
            return getPrettyName();
        }
        return prettyName;
    }

    @Override
    public void setPrettyName(String prettyName) {
        setStringValue("prettyName", prettyName);
    }

    public String getTooltip() {
        return getLargeStringValue("tooltip");
    }

    /**
     * Gets international tooltip
     * 
     * @param context
     * @return
     */
    public String getTooltip(XWikiContext context) {
        String tooltipName = getFieldFullName() + "_tooltip";
        String tooltip = context.getMessageTool().get(tooltipName);
        if (tooltipName.equals(tooltip)) {
            tooltipName = getLargeStringValue("tooltip");
            if ((tooltipName != null) && (!tooltipName.trim().equals(""))) {
                tooltip = context.getMessageTool().get(tooltipName);
            }
        }
        return tooltip;
    }

    public void setTooltip(String tooltip) {
        setLargeStringValue("tooltip", tooltip);
    }

    @Override
    public int getNumber() {
        return getIntValue("number");
    }

    @Override
    public void setNumber(int number) {
        setIntValue("number", number);
    }

    /**
     * Each type of XClass property is identified by a string that specifies the data type of the property value (e.g.
     * 'String', 'Number', 'Date') without disclosing implementation details. The internal implementation of an XClass
     * property type can change over time but its {@code classType} should not.
     * <p>
     * The {@code classType} can be used as a hint to lookup various components related to this specific XClass property
     * type. See {@link com.xpn.xwiki.internal.objects.classes.PropertyClassProvider} for instance.
     * 
     * @return an identifier for the data type of the property value (e.g. 'String', 'Number', 'Date')
     */
    public String getClassType() {
        // By default the hint is computed by removing the Class suffix, if present, from the Java simple class name
        // (without the package). Subclasses can overwrite this method to use a different hint format.
        return StringUtils.removeEnd(getClass().getSimpleName(), "Class");
    }

    /**
     * Sets the property class type.
     * 
     * @param type the class type
     * @deprecated since 4.3M1, the property class type cannot be modified
     */
    @Deprecated
    public void setClassType(String type) {
        LOGGER.warn("The property class type cannot be modified!");
    }

    @Override
    public PropertyClass clone() {
        PropertyClass pclass = (PropertyClass) super.clone();
        pclass.setObject(getObject());
        return pclass;
    }

    @Override
    public Element toXML(BaseClass bclass) {
        return toXML();
    }

    @Override
    public Element toXML() {
        Element pel = new DOMElement(getName());

        // Iterate over values sorted by field name so that the values are
        // exported to XML in a consistent order.
        Iterator it = getSortedIterator();
        while (it.hasNext()) {
            BaseProperty bprop = (BaseProperty) it.next();
            pel.add(bprop.toXML());
        }
        Element el = new DOMElement("classType");
        String classType = getClassType();
        if (this.getClass().getSimpleName().equals(classType + "Class")) {
            // Keep exporting the full Java class name for old/default property types to avoid breaking the XAR format
            // (to allow XClasses created with the current version of XWiki to be imported in an older version).
            classType = this.getClass().getName();
        }
        el.addText(classType);
        pel.add(el);
        return pel;
    }

    public void fromXML(Element pcel) throws XWikiException {
        List list = pcel.elements();
        BaseClass bclass = getxWikiClass();

        for (int i = 0; i < list.size(); i++) {
            Element ppcel = (Element) list.get(i);
            String name = ppcel.getName();
            if (bclass == null) {
                Object[] args = { getClass().getName() };
                throw new XWikiException(XWikiException.MODULE_XWIKI_CLASSES,
                        XWikiException.ERROR_XWIKI_CLASSES_PROPERTY_CLASS_IN_METACLASS,
                        "Cannot find property class {0} in MetaClass object", null, args);
            }
            PropertyClass pclass = (PropertyClass) bclass.safeget(name);
            if (pclass != null) {
                BaseProperty bprop = pclass.newPropertyfromXML(ppcel);
                bprop.setObject(this);
                safeput(name, bprop);
            }
        }
    }

    @Override
    public String toFormString() {
        return toString();
    }

    public void initLazyCollections() {
    }

    public boolean isUnmodifiable() {
        return (getIntValue("unmodifiable") == 1);
    }

    public void setUnmodifiable(boolean unmodifiable) {
        if (unmodifiable) {
            setIntValue("unmodifiable", 1);
        } else {
            setIntValue("unmodifiable", 0);
        }
    }

    /**
     * See if this property is disabled or not. A disabled property should not be editable, but existing object values
     * are still kept in the database.
     * 
     * @return {@code true} if this property is disabled and should not be used, {@code false} otherwise
     * @see #setDisabled(boolean)
     * @since 2.4M2
     */
    public boolean isDisabled() {
        return (getIntValue("disabled", 0) == 1);
    }

    /**
     * Disable or re-enable this property. A disabled property should not be editable, but existing object values are
     * still kept in the database.
     * 
     * @param disabled whether the property is disabled or not
     * @see #isDisabled()
     * @since 2.4M2
     */
    public void setDisabled(boolean disabled) {
        if (disabled) {
            setIntValue("disabled", 1);
        } else {
            setIntValue("disabled", 0);
        }
    }

    public BaseProperty fromStringArray(String[] strings) {
        return fromString(strings[0]);
    }

    public boolean isValidColumnTypes(Property hibprop) {
        return true;
    }

    @Override
    public BaseProperty fromValue(Object value) {
        BaseProperty property = newProperty();
        property.setValue(value);
        return property;
    }

    @Override
    public BaseProperty newProperty() {
        return new BaseProperty();
    }

    public void setValidationRegExp(String validationRegExp) {
        setStringValue("validationRegExp", validationRegExp);
    }

    public String getValidationRegExp() {
        return getStringValue("validationRegExp");
    }

    public String getValidationMessage() {
        return getStringValue("validationMessage");
    }

    public void setValidationMessage(String validationMessage) {
        setStringValue("validationMessage", validationMessage);
    }

    public boolean validateProperty(BaseProperty property, XWikiContext context) {
        String regexp = getValidationRegExp();
        if ((regexp == null) || (regexp.trim().equals(""))) {
            return true;
        }

        String value = ((property == null) || (property.getValue() == null)) ? "" : property.getValue().toString();
        try {
            if (context.getUtil().match(regexp, value)) {
                return true;
            }
            XWikiValidationStatus.addErrorToContext((getObject() == null) ? "" : getObject().getName(), getName(),
                    getTranslatedPrettyName(context), getValidationMessage(), context);

            return false;
        } catch (Exception e) {
            XWikiValidationStatus.addExceptionToContext((getObject() == null) ? "" : getObject().getName(),
                    getName(), e, context);

            return false;
        }
    }

    @Override
    public void flushCache() {
        this.cachedCustomDisplayer = null;
    }

    /**
     * Compares two property definitions based on their index number.
     * 
     * @param other the other property definition to be compared with
     * @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than
     *         the specified object.
     * @see #getNumber()
     * @since 2.4M2
     */
    @Override
    public int compareTo(PropertyClass other) {
        int result = this.getNumber() - other.getNumber();

        // This should never happen, but just to remove the randomness in case it does happen, also compare their names.
        if (result == 0) {
            result = this.getName().compareTo(other.getName());
        }

        return result;
    }

    protected String getFullQueryPropertyName() {
        return "obj." + getName();
    }

    /**
     * Returns the current cached default custom displayer for the PropertyClass. The result will be cached and can be
     * flushed using {@link #flushCache()}. If it returns the empty string, then there is no default custom displayer
     * for this class.
     * 
     * @param context the current request context
     * @return An identifier for the location of a custom displayer. This can be {@code class} if there's custom display
     *         code specified in the class itself, {@code page:currentwiki:XWiki.BooleanDisplayer} if such a document
     *         exists in the current wiki, {@code page:xwiki:XWiki.StringDisplayer} if such a document exists in the
     *         main wiki, or {@code template:displayer_boolean.vm} if a template on the filesystem or in the current
     *         skin exists.
     */
    protected String getCachedDefaultCustomDisplayer(XWikiContext context) {
        // First look at custom displayer in class. We should not cache this one.
        String customDisplay = getCustomDisplay();
        if (StringUtils.isNotEmpty(customDisplay)) {
            return CLASS_DISPLAYER_IDENTIFIER;
        }

        // Then look for pages or templates
        if (this.cachedCustomDisplayer == null) {
            this.cachedCustomDisplayer = getDefaultCustomDisplayer(getTypeName(), context);
        }
        return this.cachedCustomDisplayer;
    }

    /**
     * Method to find the default custom displayer to use for a specific Property Class.
     * 
     * @param propertyClassName the type of the property; this is defined in each subclass, such as {@code boolean},
     *        {@code string} or {@code dblist}
     * @param context the current request context
     * @return An identifier for the location of a custom displayer. This can be {@code class} if there's custom display
     *         code specified in the class itself, {@code page:currentwiki:XWiki.BooleanDisplayer} if such a document
     *         exists in the current wiki, {@code page:xwiki:XWiki.StringDisplayer} if such a document exists in the
     *         main wiki, or {@code template:displayer_boolean.vm} if a template on the filesystem or in the current
     *         skin exists.
     */
    protected String getDefaultCustomDisplayer(String propertyClassName, XWikiContext context) {
        LOGGER.debug("Looking up default custom displayer for property class name [{}]", propertyClassName);

        try {
            // First look into the current wiki
            String pageName = StringUtils.capitalize(propertyClassName) + "Displayer";
            DocumentReference reference = new DocumentReference(context.getDatabase(), "XWiki", pageName);
            if (context.getWiki().exists(reference, context)) {
                LOGGER.debug("Found default custom displayer for property class name in local wiki: [{}]",
                        pageName);
                return DOCUMENT_DISPLAYER_IDENTIFIER_PREFIX + "XWiki." + pageName;
            }

            // Look in the main wiki
            if (context.getWiki().isVirtualMode()
                    && !StringUtils.equals(context.getDatabase(), context.getMainXWiki())) {
                reference = new DocumentReference(context.getMainXWiki(), "XWiki", pageName);
                if (context.getWiki().exists(reference, context)) {
                    LOGGER.debug("Found default custom displayer for property class name in main wiki: [{}]",
                            pageName);
                    return DOCUMENT_DISPLAYER_IDENTIFIER_PREFIX + context.getMainXWiki() + ":XWiki." + pageName;
                }
            }

            // Look in templates
            String template = "displayer_" + propertyClassName + ".vm";
            String result = "";
            try {
                result = context.getWiki().evaluateTemplate(template, context);
                if (StringUtils.isNotEmpty(result)) {
                    LOGGER.debug("Found default custom displayer for property class name as template: [{}]",
                            template);
                    return TEMPLATE_DISPLAYER_IDENTIFIER_PREFIX + template;
                }
            } catch (IOException e) {
            }
        } catch (Throwable e) {
            // If we fail we consider there is no custom displayer
            LOGGER.error("Error while trying to evaluate if a property has a custom displayer", e);
        }

        return null;
    }

    /**
     * Get a short name identifying this type of property. This is derived from the java class name, lowercasing the
     * part before {@code Class}.
     * 
     * @return a string, for example {@code string}, {@code dblist}, {@code number}
     */
    private String getTypeName() {
        return StringUtils.substringBeforeLast(this.getClass().getSimpleName(), "Class").toLowerCase();
    }
}