/* * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Nicolas Chapurlat <nchapurlat@nuxeo.com> */ package org.nuxeo.ecm.core.api.validation; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.utils.i18n.I18NUtils; import org.nuxeo.ecm.core.schema.types.Field; import org.nuxeo.ecm.core.schema.types.Schema; import org.nuxeo.ecm.core.schema.types.constraints.Constraint; /** * A constraint violation description. Use {@link #getMessage(Locale)} to get the constraint violation description. * <p> * You could customize constraint violation message using the following rules : * <ul> * <li>Use {@value #MESSAGES_KEY} key in {@value #MESSAGES_BUNDLE} bundle to customize default message</li> * <li>Append the constraint name to the previous key to customize the generic message to some constraint</li> * <li>Append the schema and the field name to the previous key to customize the message for a specific constraint * applied to some specific schema field.</li> * </ul> * <br> * For each messages, you can use parameters in the message : * <ul> * <li>The invalid value : {0}</li> * <li>The schema name : {1}</li> * <li>The field name : {2}</li> * <li>The constraint name : {3}</li> * <li>The first constraint parameter (if exists) : {4}</li> * <li>The second constraint parameter (if exists) : {5}</li> * <li>...</li> * </ul> * </p> * <p> * Examples : * <ul> * <li>label.schema.constraint.violation=Value '{0}' for field '{1}.{2}' does not respect constraint '{3}'</li> * <li>label.schema.constraint.violation.PatternConstraint='{1}.{2}' value ({0}) should match the following format : * '{4}'</li> * <li>label.schema.constraint.violation.PatternConstraint.myuserschema.firstname ='The firstname should not be empty'</li> * </ul> * </p> * * @since 7.1 */ public class ConstraintViolation implements Serializable { private static final Log log = LogFactory.getLog(ConstraintViolation.class); private static final long serialVersionUID = 1L; private final Schema schema; private final List<PathNode> path; private final Constraint constraint; private final Object invalidValue; public ConstraintViolation(Schema schema, List<PathNode> fieldPath, Constraint constraint, Object invalidValue) { this.schema = schema; path = new ArrayList<PathNode>(fieldPath); this.constraint = constraint; this.invalidValue = invalidValue; } public Schema getSchema() { return schema; } public List<PathNode> getPath() { return Collections.unmodifiableList(path); } public Constraint getConstraint() { return constraint; } public Object getInvalidValue() { return invalidValue; } /** * @return The message if it's found in message bundles, a generic message otherwise. * @since 7.1 */ public String getMessage(Locale locale) { // test whether there's a specific translation for for this field and this constraint // the expected key is label.schema.constraint.violation.[constraintName].[schemaName].[field].[subField] List<String> pathTokens = new ArrayList<String>(); pathTokens.add(Constraint.MESSAGES_KEY); pathTokens.add(constraint.getDescription().getName()); pathTokens.add(schema.getName()); for (PathNode node : path) { String name = node.getField().getName().getLocalName(); pathTokens.add(name); } String key = StringUtils.join(pathTokens, '.'); String computedInvalidValue = "null"; if (invalidValue != null) { String invalidValueString = invalidValue.toString(); if (invalidValueString.length() > 20) { computedInvalidValue = invalidValueString.substring(0, 15) + "..."; } else { computedInvalidValue = invalidValueString; } } Object[] params = new Object[] { computedInvalidValue }; Locale computedLocale = locale != null ? locale : Constraint.MESSAGES_DEFAULT_LANG; String message = null; try { message = I18NUtils.getMessageString(Constraint.MESSAGES_BUNDLE, key, params, computedLocale); } catch (MissingResourceException e) { log.trace("No bundle found", e); message = null; } if (message != null && !message.trim().isEmpty() && !key.equals(message)) { // use the message if there's one return message; } else { if (locale != null && Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { // use the constraint message return constraint.getErrorMessage(invalidValue, locale); } else { return getMessage(Locale.ENGLISH); } } } @Override public String toString() { return getMessage(Locale.ENGLISH); } /** * Allows to locates some constraint violation in a document. * <p> * {@link #getIndex()} are used to indicates which element violates the constraint for list properties. * </p> * * @since 7.1 */ public static class PathNode { private Field field; private boolean listItem = false; int index = 0; public PathNode(Field field) { this.field = field; } public PathNode(Field field, int index) { super(); this.field = field; this.index = index; listItem = true; } public Field getField() { return field; } public int getIndex() { return index; } public boolean isListItem() { return listItem; } @Override public String toString() { if (listItem) { return field.getName().getPrefixedName(); } else { return field.getName().getPrefixedName() + "[" + index + "]"; } } } }