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}