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