/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.core.gui.configuration.helper;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.el.ValueExpression;
import javax.faces.application.FacesMessage;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.UIOutput;
import javax.faces.component.UISelectItem;
import javax.faces.component.UISelectOne;
import javax.faces.component.html.HtmlInputSecret;
import javax.faces.component.html.HtmlInputText;
import javax.faces.component.html.HtmlInputTextarea;
import javax.faces.component.html.HtmlSelectBooleanCheckbox;
import javax.faces.component.html.HtmlSelectOneMenu;
import javax.faces.component.html.HtmlSelectOneRadio;
import javax.faces.context.FacesContext;
import javax.faces.validator.ValidatorException;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import org.rhq.core.domain.configuration.DynamicConfigurationPropertyValue;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertyDefinitionDynamic;
import org.rhq.core.domain.configuration.PropertyDynamicType;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.configuration.definition.PropertyDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionEnumeration;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple;
import org.rhq.core.domain.configuration.definition.PropertySimpleType;
import org.rhq.core.gui.configuration.CssStyleClasses;
import org.rhq.core.gui.converter.PropertySimpleValueConverter;
import org.rhq.core.gui.util.FacesComponentUtility;
import org.rhq.core.gui.util.FacesContextUtility;
import org.rhq.core.gui.util.PropertyIdGeneratorUtility;
import org.rhq.core.gui.validator.PropertySimpleValueValidator;
import org.rhq.core.template.TemplateEngine;

/**
 * A class that provides methods for rendering RHQ {@link Property}s.
 *
 * @author Ian Springer
 */
public class PropertyRenderingUtility {
    private static final int INPUT_TEXT_COMPONENT_WIDTH = 30;
    private static final int INPUT_TEXTAREA_COMPONENT_ROWS = 4;
    private static final String ERROR_MSG_STYLE_CLASS = "error-msg";

    /**
     * Enums with a size equal to or greater than this threshold will be rendered as list boxes, rather than radios.
     */
    private static final int LISTBOX_THRESHOLD_ENUM_SIZE = 6;

    private static final String UNIQUE_ID_PREFIX = "rhq_id";

    /**
     * Holds a mapping from dynamic property backing type (i.e. database, server-side-plugin) to the
     * {@link DynamicPropertyRetriever} instance that should be called to retrieve the values. Code using
     * this class as a core UI dependency
     */
    private static final Map<PropertyDynamicType, DynamicPropertyRetriever> DYNAMIC_PROPERTY_RETRIEVERS =
        new HashMap<PropertyDynamicType, DynamicPropertyRetriever>();

    @NotNull
    public static UIInput createInputForSimpleProperty(PropertyDefinitionSimple propertyDefinitionSimple,
        PropertySimple propertySimple, ValueExpression propertyValueExpression, Integer listIndex,
        boolean isGroupConfig, boolean configReadOnly, boolean configFullyEditable, boolean prevalidate, TemplateEngine templateEngine) {
        UIInput input = createInput(propertyDefinitionSimple);

        if (propertySimple != null)
            addTitleAttribute(input, propertySimple.getStringValue());

        boolean isUnset = isUnset(propertyDefinitionSimple.isRequired(), propertySimple, isGroupConfig);
        boolean isReadOnly = isReadOnly(propertyDefinitionSimple.isReadOnly(), propertyDefinitionSimple.isRequired(),
            propertySimple, configReadOnly, configFullyEditable);

        String propertyId = PropertyIdGeneratorUtility.getIdentifier(propertySimple, listIndex);
        input.setId(propertyId);

        input.setValueExpression("value", propertyValueExpression);

        FacesComponentUtility.setUnset(input, isUnset);
        FacesComponentUtility.setReadonly(input, isReadOnly);

        addValidatorsAndConverter(input, propertyDefinitionSimple, configReadOnly, templateEngine);

        addErrorMessages(input, propertyDefinitionSimple, propertySimple, prevalidate);
        return input;
    }

    public static UIInput createInputForDynamicProperty(PropertyDefinitionDynamic propertyDefinitionDynamic,
        PropertySimple property, ValueExpression valueExpression, Integer listIndex, boolean isGroupConfig,
        boolean configReadOnly, boolean configFullyEditable, boolean prevalidate) {

        // Simply use a drop down in all cases
        UISelectOne input = FacesComponentUtility.createComponent(HtmlSelectOneMenu.class, null);

        // Determine where to retrieve the values from
        PropertyDynamicType propertyType = propertyDefinitionDynamic.getDynamicType();
        DynamicPropertyRetriever retriever = DYNAMIC_PROPERTY_RETRIEVERS.get(propertyType);

        if (retriever == null) {
            throw new IllegalStateException("Attempt to render a dynamic property but no retrievers are " +
                "configured for the type. Dynamic property type: " + propertyType + ", Definition: " +
                propertyDefinitionDynamic);
        }

        // Retrieve the values and load them into UI options
        List<DynamicConfigurationPropertyValue> values = retriever.loadValues(propertyDefinitionDynamic);
        for (DynamicConfigurationPropertyValue option : values) {
            UISelectItem selectItem = FacesComponentUtility.createComponent(UISelectItem.class, null);
            selectItem.setItemLabel(option.getDisplay());
            selectItem.setItemValue(option.getValue());
            input.getChildren().add(selectItem);
        }

        boolean isUnset = isUnset(propertyDefinitionDynamic.isRequired(), property, isGroupConfig);
        boolean isReadOnly = isReadOnly(propertyDefinitionDynamic.isReadOnly(), propertyDefinitionDynamic.isRequired(),
            property, configReadOnly, configFullyEditable);

        String propertyId = PropertyIdGeneratorUtility.getIdentifier(property, listIndex);
        input.setId(propertyId);

        // Revert to the previously selected value
        input.setValueExpression("value", valueExpression);

        FacesComponentUtility.setUnset(input, isUnset);
        FacesComponentUtility.setReadonly(input, isReadOnly);

        // If there is a PC-detected error associated with the property, associate it with the input.
        addPluginContainerDetectedErrorMessage(input, property);

        return input;
    }

    /**
     * Sets the object used to retrieve property values for dynamic properties of the given type.
     *
     * @param type      each type in the enum will only have one retriever associated with it at any given time
     * @param retriever may not be <code>null</code>
     */
    public static void putDynamicPropertyRetriever(PropertyDynamicType type, DynamicPropertyRetriever retriever) {
        DYNAMIC_PROPERTY_RETRIEVERS.put(type, retriever);
    }

    public static UIInput createInput(PropertyDefinitionSimple propertyDefinitionSimple) {
        UIInput input;
        PropertySimpleType type = (propertyDefinitionSimple != null) ? propertyDefinitionSimple.getType()
            : PropertySimpleType.STRING;
        switch (type) {
        case BOOLEAN: {
            input = createInputForBooleanProperty();
            break;
        }

        case LONG_STRING: {
            input = createInputForLongStringProperty();
            break;
        }

        case PASSWORD: {
            input = createInputForPasswordProperty();
            break;
        }

        default: {
            if (propertyDefinitionSimple != null && isEnum(propertyDefinitionSimple)) {
                input = createInputForEnumProperty(propertyDefinitionSimple);
            } else {
                input = createInputForStringProperty();
            }
        }
        }
        input.setId(UNIQUE_ID_PREFIX + UUID.randomUUID());

        if (propertyDefinitionSimple != null) {
            // It's important to set the label attribute, since we include it in our validation error messages.
            input.getAttributes().put("label", propertyDefinitionSimple.getDisplayName());

            // The below adds an inert attribute to the input that contains the name of the associated property - useful
            // for debugging (i.e. when viewing source of the page or using a JavaScript debugger).
            input.getAttributes().put("ondblclick", "//" + propertyDefinitionSimple.getName());
        }
        return input;
    }

    @NotNull
    public static UIInput createInputForSimpleProperty(PropertySimple propertySimple, ValueExpression valueExpression,
        boolean readOnly) {
        UIInput input = createInputForStringProperty();
        input.setValueExpression("value", valueExpression);
        FacesComponentUtility.setReadonly(input, readOnly);
        addTitleAttribute(input, propertySimple.getStringValue());
        return input;
    }

    public static void addMessageComponentForInput(UIComponent parent, UIInput input) {
        // <h:message for="#{input-component-id}" showDetail="true" errorClass="error-msg" />
        FacesComponentUtility.addMessage(parent, null, input.getId(), ERROR_MSG_STYLE_CLASS);
        // TODO: specify a component id
    }

    public static HtmlSelectBooleanCheckbox addUnsetControl(UIComponent parent,
        boolean isRequired, boolean isReadOnly, PropertySimple propertySimple, Integer listIndex,
        UIInput valueInput, boolean isGroupConfig, boolean configReadOnly, boolean configFullyEditable) {

        HtmlSelectBooleanCheckbox unsetCheckbox = FacesComponentUtility.createComponent(
            HtmlSelectBooleanCheckbox.class, null);
        if (propertySimple != null) {
            String unsetCheckboxId = PropertyIdGeneratorUtility.getIdentifier(propertySimple, listIndex, "Unset");
            unsetCheckbox.setId(unsetCheckboxId);
        }
        parent.getChildren().add(unsetCheckbox);
        unsetCheckbox.setValue(isUnset(isRequired, propertySimple, isGroupConfig));
        if (isReadOnly(isReadOnly, isRequired, propertySimple, configReadOnly, configFullyEditable)
            || isGroupConfigWithDifferingValues(propertySimple, isGroupConfig)) {
            FacesComponentUtility.setDisabled(unsetCheckbox, true);
        } else {
            // Add JavaScript that will disable/enable the corresponding input element when the unset checkbox is
            // checked/unchecked.
            // IMPORTANT: We must use document.formName.inputName, rather than document.getElementById('inputId'),
            //            to reference the HTML DOM element, because the id of the HTML DOM input element is not the same as the
            //            id of the corresponding JSF input component in some cases (e.g. radio buttons). However, the
            //            name property that JSF renders on the HTML DOM input element does always match the JSF
            //            component id. (ips, 05/31/07)
            StringBuilder onclick = new StringBuilder();
            for (String htmlDomReference : getHtmlDomReferences(valueInput)) {
                onclick.append("setInputUnset(").append(htmlDomReference).append(", this.checked);");
            }
            unsetCheckbox.setOnclick(onclick.toString());
        }
        return unsetCheckbox;
    }

    public static void addInitInputsJavaScript(UIComponent parent, String componentId, boolean configFullyEditable,
        boolean postBack) {
        List<UIInput> inputs = FacesComponentUtility.getDescendantsOfType(parent, UIInput.class);
        List<UIInput> overrideInputs = new ArrayList<UIInput>();
        List<UIInput> unsetInputs = new ArrayList<UIInput>();
        List<UIInput> readOnlyInputs = new ArrayList<UIInput>();
        for (UIInput input : inputs) {
            // readOnly components can not be overridden - by this point, the override
            // status should have only been set if the component *was not* readOnly
            //if (FacesComponentUtility.isOverride(input)) {
            //    overrideInputs.add(input);
            //}

            if (postBack) {
                //we need to do a check for both null and NULL_INPUT_VALUE here.
                //The null value is present in the stored state in the session 
                //(that is what the NULL_INPUT_VALUE becomes after processing with PropertySimpleValueConverter)
                //where as NULL_INPUT_VALUE is actually submitted from the client.
                //Because this is a common method that processes the page regardless
                //of where the input has come from, we need to check for both possible values.
                boolean inputIsNull = input.getSubmittedValue() == null || PropertySimpleValueConverter.NULL_INPUT_VALUE.equals(input.getSubmittedValue());
                FacesComponentUtility.setUnset(input, inputIsNull);
            }
            if (FacesComponentUtility.isUnset(input)) {
                unsetInputs.add(input);
            }

            if (!configFullyEditable && FacesComponentUtility.isReadonly(input)) {
                readOnlyInputs.add(input);
            }
        }

        StringBuilder script = new StringBuilder();

        if (!overrideInputs.isEmpty()) {
            script.append("var overrideInputArray = new Array(");
            for (UIInput input : overrideInputs) {
                for (String htmlDomReference : getHtmlDomReferences(input)) {
                    script.append(htmlDomReference).append(", ");
                }
            }

            script.delete(script.length() - 2, script.length()); // chop off the extra ", "
            script.append(");\n");

            // do it this way instead of DISABLED attribute via code, because if DISABLED is used and
            // even if javascript later enables them, JSF won't submit them as part of the component
            script.append("setInputsOverride(overrideInputArray, false);\n");
        }

        if (!unsetInputs.isEmpty()) {
            script.append("var unsetInputArray = new Array(");
            for (UIInput input : unsetInputs) {
                for (String htmlDomReference : getHtmlDomReferences(input)) {
                    script.append(htmlDomReference).append(", ");
                }
            }

            script.delete(script.length() - 2, script.length()); // chop off the extra ", "
            script.append(");\n");

            // do it this way instead of DISABLED attribute via code, because if DISABLED is used and
            // even if javascript later enables them, JSF won't submit them as part of the component
            script.append("unsetInputs(unsetInputArray);\n");
        }

        if (!readOnlyInputs.isEmpty()) {
            script.append("var readOnlyInputArray = new Array(");
            for (UIInput input : readOnlyInputs) {
                for (String htmlDomReference : getHtmlDomReferences(input)) {
                    script.append(htmlDomReference).append(", ");
                }
            }

            script.delete(script.length() - 2, script.length()); // chop off the extra ", "
            script.append(");\n");
            script.append("writeProtectInputs(readOnlyInputArray);\n");
        }

        UIOutput uiOutput = FacesComponentUtility.addJavaScript(parent, null, null, script);

        uiOutput.setId(componentId);
    }

    public static List<String> getHtmlDomReferences(UIComponent component) {
        List<String> htmlDomReferences = new ArrayList<String>();
        if (component instanceof HtmlSelectOneRadio) {
            String clientId = component.getClientId(FacesContext.getCurrentInstance());
            int selectItemCount = 0;
            for (UIComponent child : component.getChildren()) {
                if (child instanceof UISelectItem) {
                    String selectItemClientId = clientId + NamingContainer.SEPARATOR_CHAR + selectItemCount++;
                    htmlDomReferences.add(getHtmlDomReference(selectItemClientId));
                } else {
                    throw new IllegalStateException(
                        "HtmlSelectOneRadio component has a child that is not a UISelectItem.");
                }
            }
        } else {
            String clientId = component.getClientId(FacesContext.getCurrentInstance());
            htmlDomReferences.add(getHtmlDomReference(clientId));
        }

        return htmlDomReferences;
    }

    // <h:outputLabel value="DISPLAY_NAME" styleClass="..." />
    public static void addPropertyDisplayName(UIComponent parent, PropertyDefinition propertyDefinition,
        Property property, boolean configReadOnly) {
        String displayName = (propertyDefinition != null) ? propertyDefinition.getDisplayName() : property.getName();
        FacesComponentUtility.addOutputText(parent, null, displayName, CssStyleClasses.PROPERTY_DISPLAY_NAME_TEXT);
        if (!configReadOnly && propertyDefinition != null &&
            propertyDefinition.isRequired() &&
               (propertyDefinition instanceof PropertyDefinitionSimple ||
                propertyDefinition instanceof PropertyDefinitionDynamic) ) {
            // Print a required marker next to required simples.
            // Ignore the required field for maps and lists, as it is has no significance for them.
            FacesComponentUtility.addOutputText(parent, null, " * ", CssStyleClasses.REQUIRED_MARKER_TEXT);
        }
    }

    public static void addPropertyDescription(UIComponent parent, PropertyDefinition propertyDefinition) {
        // <span class="description">DESCRIPTION</span>
        FacesComponentUtility.addOutputText(parent, null, propertyDefinition.getDescription(),
            CssStyleClasses.DESCRIPTION);
    }

    static String getHtmlDomReference(String clientId) {
        return "document.getElementById('" + clientId + "')";
    }

    private static UIInput createInputForBooleanProperty() {
        // <h:selectOneRadio id="#{identifier}" value="#{beanValue}" layout="pageDirection" styleClass="radiolabels">
        //    <f:selectItems value="#{itemValues}"></f:selectItems>
        // </h:selectOneRadio>
        HtmlSelectOneRadio selectOneRadio = FacesComponentUtility.createComponent(HtmlSelectOneRadio.class, null);
        selectOneRadio.setLayout("lineDirection");
        // TODO: We may want to use CSS to get less space between the radio buttons
        //      (see http://jira.jboss.com/jira/browse/JBMANCON-21).

        UISelectItem selectItem = FacesComponentUtility.createComponent(UISelectItem.class, null);
        selectItem.setItemLabel("Yes");
        selectItem.setItemValue("true");
        selectOneRadio.getChildren().add(selectItem);
        selectItem = FacesComponentUtility.createComponent(UISelectItem.class, null);
        selectItem.setItemLabel("No");
        selectItem.setItemValue("false");
        selectOneRadio.getChildren().add(selectItem);
        return selectOneRadio;
    }

    // <h:selectOneRadio id="#{identifier}" value="#{beanValue}" layout="pageDirection" styleClass="radiolabels">
    //    <f:selectItems value="#{itemValues}"></f:selectItems>
    // </h:selectOneRadio>
    private static UIInput createInputForEnumProperty(PropertyDefinitionSimple propertyDefinitionSimple) {
        UISelectOne selectOne;
        if (propertyDefinitionSimple.getEnumeratedValues().size() >= LISTBOX_THRESHOLD_ENUM_SIZE) {
            // Use a drop down menu for larger enums...
            HtmlSelectOneMenu menu = FacesComponentUtility.createComponent(HtmlSelectOneMenu.class, null);

            // TODO: Use CSS to set the width of the menu.
            selectOne = menu;
        } else {
            // ...and a radio for smaller ones.
            HtmlSelectOneRadio radio = FacesComponentUtility.createComponent(HtmlSelectOneRadio.class, null);
            radio.setLayout("pageDirection");

            // TODO: We may want to use CSS to get less space between the radio buttons
            //      (see http://jira.jboss.com/jira/browse/JBMANCON-21).
            selectOne = radio;
        }

        List<PropertyDefinitionEnumeration> options = propertyDefinitionSimple.getEnumeratedValues();
        for (PropertyDefinitionEnumeration option : options) {
            UISelectItem selectItem = FacesComponentUtility.createComponent(UISelectItem.class, null);
            selectItem.setItemLabel(option.getName());
            selectItem.setItemValue(option.getValue());
            selectOne.getChildren().add(selectItem);
        }

        return selectOne;
    }

    private static UIInput createInputForStringProperty() {
        HtmlInputText inputText = FacesComponentUtility.createComponent(HtmlInputText.class, null);

        //TODO: check if this has units, then apply the correct style
        inputText.setStyleClass(CssStyleClasses.PROPERTY_VALUE_INPUT);
        //      inputText.setStyle(INPUT_TEXT_WIDTH_STYLE_WITH_UNITS);
        inputText.setMaxlength(PropertySimple.MAX_VALUE_LENGTH);

        // Disable browser auto-completion.
        inputText.setAutocomplete("off");
        return inputText;
    }

    private static UIInput createInputForPasswordProperty() {
        HtmlInputSecret inputSecret = FacesComponentUtility.createComponent(HtmlInputSecret.class, null);
        inputSecret.setStyleClass(CssStyleClasses.PROPERTY_VALUE_INPUT);
        inputSecret.setMaxlength(PropertySimple.MAX_VALUE_LENGTH);

        // TODO: Remove the below line, as it's not secure, and improve support for displaying/validating password fields.
        inputSecret.setRedisplay(true);

        // Disable browser auto-completion.
        inputSecret.setAutocomplete("off");
        return inputSecret;
    }

    private static UIInput createInputForLongStringProperty() {
        HtmlInputTextarea inputTextarea = FacesComponentUtility.createComponent(HtmlInputTextarea.class, null);
        inputTextarea.setRows(INPUT_TEXTAREA_COMPONENT_ROWS);
        inputTextarea.setStyleClass(CssStyleClasses.PROPERTY_VALUE_INPUT);
        return inputTextarea;
    }

    private static boolean isEnum(PropertyDefinitionSimple simplePropertyDefinition) {
        return !simplePropertyDefinition.getEnumeratedValues().isEmpty();
    }

    private static void addValidatorsAndConverter(UIInput input, PropertyDefinitionSimple propertyDefinitionSimple,
        boolean readOnly,TemplateEngine templateEngine ) {
        if (!readOnly) {
            input.setRequired(propertyDefinitionSimple.isRequired());
            input.addValidator(new PropertySimpleValueValidator(propertyDefinitionSimple));
            input.setConverter(new PropertySimpleValueConverter(templateEngine));
        }
    }

    private static void addErrorMessages(UIInput input, @Nullable PropertyDefinitionSimple propertyDefinitionSimple,
        PropertySimple propertySimple, boolean prevalidate) {
        if (prevalidate) {
            // Pre-validate the property's value, in case the PC sent us an invalid live config.
            PropertySimpleValueValidator validator = new PropertySimpleValueValidator(propertyDefinitionSimple);
            //PropertySimple propertySimple = this.propertyMap.getSimple(propertyDefinitionSimple.getName());
            prevalidatePropertyValue(input, propertySimple, validator);
        }
        // If there is a PC-detected error associated with the property, associate it with the input.
        addPluginContainerDetectedErrorMessage(input, propertySimple);
    }

    private static void prevalidatePropertyValue(UIInput propertyValueInput, PropertySimple propertySimple,
        PropertySimpleValueValidator validator) {
        FacesContext facesContext = FacesContextUtility.getFacesContext();
        try {
            String value = (propertySimple != null) ? propertySimple.getStringValue() : null;
            validator.validate(facesContext, propertyValueInput, value);
        } catch (ValidatorException e) {
            // NOTE: It's vital to pass the client id, *not* the component id, to addMessage().
            facesContext.addMessage(propertyValueInput.getClientId(facesContext), e.getFacesMessage());
        }
    }

    private static void addPluginContainerDetectedErrorMessage(UIInput input, PropertySimple propertySimple) {
        String errorMsg = (propertySimple != null) ? propertySimple.getErrorMessage() : null;
        if ((errorMsg != null) && !errorMsg.equals("")) {
            FacesContext facesContext = FacesContextUtility.getFacesContext();
            FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, errorMsg, null);

            // NOTE: It's vital to pass the client id, *not* the component id, to addMessage().
            facesContext.addMessage(input.getClientId(facesContext), facesMsg);
        }
    }

    private static void addTitleAttribute(UIInput input, String propertyValue) {
        if (input instanceof HtmlInputText) {
            //         TODO: will this still work now that we use the style def'n "width:185px;" to define the input field width?
            // For text inputs with values that are too long to fit in the input text field, add a "title" attribute set to
            // the value, so the user can see the untruncated value via a tooltip.
            // (see http://jira.jboss.com/jira/browse/JBNADM-1608)
            HtmlInputText inputText = (HtmlInputText) input;
            if ((propertyValue != null) && (propertyValue.length() > INPUT_TEXT_COMPONENT_WIDTH))
                inputText.setTitle(propertyValue);
            inputText.setOnchange("setInputTitle(this)");
        }
    }

    private static boolean isUnset(boolean isRequired, PropertySimple propertySimple,
        boolean isGroupConfig) {
        if (isGroupConfigWithDifferingValues(propertySimple, isGroupConfig))
            // Properties from group configs that have differing values should not be marked unset.
            return false;
        if (propertySimple == null)
            return false;

        return (!isRequired && propertySimple.getStringValue() == null);
    }

    public static boolean isReadOnly(boolean isReadOnly, boolean isRequired, PropertySimple propertySimple,
        boolean configReadOnly, boolean configFullyEditable) {
        // A fully editable config overrides any other means of setting read-only.
        return (!configFullyEditable && //
                    (configReadOnly || //
                     isReadOnly && //
                        (propertySimple == null || //
                         !isInvalidRequiredProperty(isRequired, propertySimple))));
    }

    public static boolean isGroupConfigWithDifferingValues(PropertySimple propertySimple, boolean isGroupConfig) {
        return isGroupConfig && (propertySimple.getOverride() == null || !propertySimple.getOverride());
    }

    private static boolean isInvalidRequiredProperty(boolean isRequired, PropertySimple propertySimple) {
        boolean isInvalidRequiredProperty = false;
        if (isRequired) {
            String errorMessage = propertySimple.getErrorMessage();
            if ((null == propertySimple.getStringValue()) || "".equals(propertySimple.getStringValue())
                || ((null != errorMessage) && (!"".equals(errorMessage.trim())))) {
                // Required properties with no value, or an invalid value (assumed if we see an error message) should
                // never be set to read-only, otherwise the user will have no way to give the property a value and
                // thereby get things to a valid state.
                isInvalidRequiredProperty = true;
            }
        }
        return isInvalidRequiredProperty;
    }
}
