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}