001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
018 */
019
020package org.nuxeo.ecm.core.api.validation;
021
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.List;
026import java.util.Locale;
027import java.util.MissingResourceException;
028
029import org.apache.commons.lang.StringUtils;
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.common.utils.i18n.I18NUtils;
033import org.nuxeo.ecm.core.schema.types.Field;
034import org.nuxeo.ecm.core.schema.types.Schema;
035import org.nuxeo.ecm.core.schema.types.constraints.Constraint;
036
037/**
038 * A constraint violation description. Use {@link #getMessage(Locale)} to get the constraint violation description.
039 * <p>
040 * You could customize constraint violation message using the following rules :
041 * <ul>
042 * <li>Use {@value #MESSAGES_KEY} key in {@value #MESSAGES_BUNDLE} bundle to customize default message</li>
043 * <li>Append the constraint name to the previous key to customize the generic message to some constraint</li>
044 * <li>Append the schema and the field name to the previous key to customize the message for a specific constraint
045 * applied to some specific schema field.</li>
046 * </ul>
047 * <br>
048 * For each messages, you can use parameters in the message :
049 * <ul>
050 * <li>The invalid value : {0}</li>
051 * <li>The schema name : {1}</li>
052 * <li>The field name : {2}</li>
053 * <li>The constraint name : {3}</li>
054 * <li>The first constraint parameter (if exists) : {4}</li>
055 * <li>The second constraint parameter (if exists) : {5}</li>
056 * <li>...</li>
057 * </ul>
058 * </p>
059 * <p>
060 * Examples :
061 * <ul>
062 * <li>label.schema.constraint.violation=Value '{0}' for field '{1}.{2}' does not respect constraint '{3}'</li>
063 * <li>label.schema.constraint.violation.PatternConstraint='{1}.{2}' value ({0}) should match the following format :
064 * '{4}'</li>
065 * <li>label.schema.constraint.violation.PatternConstraint.myuserschema.firstname ='The firstname should not be empty'</li>
066 * </ul>
067 * </p>
068 *
069 * @since 7.1
070 */
071public class ConstraintViolation implements Serializable {
072
073    private static final Log log = LogFactory.getLog(ConstraintViolation.class);
074
075    private static final long serialVersionUID = 1L;
076
077    private final Schema schema;
078
079    private final List<PathNode> path;
080
081    private final Constraint constraint;
082
083    private final Object invalidValue;
084
085    public ConstraintViolation(Schema schema, List<PathNode> fieldPath, Constraint constraint, Object invalidValue) {
086        this.schema = schema;
087        path = new ArrayList<PathNode>(fieldPath);
088        this.constraint = constraint;
089        this.invalidValue = invalidValue;
090    }
091
092    public Schema getSchema() {
093        return schema;
094    }
095
096    public List<PathNode> getPath() {
097        return Collections.unmodifiableList(path);
098    }
099
100    public Constraint getConstraint() {
101        return constraint;
102    }
103
104    public Object getInvalidValue() {
105        return invalidValue;
106    }
107
108    /**
109     * @return The message if it's found in message bundles, a generic message otherwise.
110     * @since 7.1
111     */
112    public String getMessage(Locale locale) {
113        // test whether there's a specific translation for for this field and this constraint
114        // the expected key is label.schema.constraint.violation.[constraintName].[schemaName].[field].[subField]
115        List<String> pathTokens = new ArrayList<String>();
116        pathTokens.add(Constraint.MESSAGES_KEY);
117        pathTokens.add(constraint.getDescription().getName());
118        pathTokens.add(schema.getName());
119        for (PathNode node : path) {
120            String name = node.getField().getName().getLocalName();
121            pathTokens.add(name);
122        }
123        String key = StringUtils.join(pathTokens, '.');
124        String computedInvalidValue = "null";
125        if (invalidValue != null) {
126            String invalidValueString = invalidValue.toString();
127            if (invalidValueString.length() > 20) {
128                computedInvalidValue = invalidValueString.substring(0, 15) + "...";
129            } else {
130                computedInvalidValue = invalidValueString;
131            }
132        }
133        Object[] params = new Object[] { computedInvalidValue };
134        Locale computedLocale = locale != null ? locale : Constraint.MESSAGES_DEFAULT_LANG;
135        String message = null;
136        try {
137            message = I18NUtils.getMessageString(Constraint.MESSAGES_BUNDLE, key, params, computedLocale);
138        } catch (MissingResourceException e) {
139            log.trace("No bundle found", e);
140            message = null;
141        }
142        if (message != null && !message.trim().isEmpty() && !key.equals(message)) {
143            // use the message if there's one
144            return message;
145        } else {
146            if (locale != null && Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
147                // use the constraint message
148                return constraint.getErrorMessage(invalidValue, locale);
149            } else {
150                return getMessage(Locale.ENGLISH);
151            }
152        }
153    }
154
155    @Override
156    public String toString() {
157        return getMessage(Locale.ENGLISH);
158    }
159
160    /**
161     * Allows to locates some constraint violation in a document.
162     * <p>
163     * {@link #getIndex()} are used to indicates which element violates the constraint for list properties.
164     * </p>
165     *
166     * @since 7.1
167     */
168    public static class PathNode {
169
170        private Field field;
171
172        private boolean listItem = false;
173
174        int index = 0;
175
176        public PathNode(Field field) {
177            this.field = field;
178        }
179
180        public PathNode(Field field, int index) {
181            super();
182            this.field = field;
183            this.index = index;
184            listItem = true;
185        }
186
187        public Field getField() {
188            return field;
189        }
190
191        public int getIndex() {
192            return index;
193        }
194
195        public boolean isListItem() {
196            return listItem;
197        }
198
199        @Override
200        public String toString() {
201            if (listItem) {
202                return field.getName().getPrefixedName();
203            } else {
204                return field.getName().getPrefixedName() + "[" + index + "]";
205            }
206        }
207
208    }
209
210}