001/* 002 * (C) Copyright 2014-2018 Nuxeo (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 <[email protected]> 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.lang3.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' 066 * </li> 067 * </ul> 068 * </p> 069 * 070 * @since 7.1 071 */ 072public class ConstraintViolation implements Serializable { 073 074 private static final Log log = LogFactory.getLog(ConstraintViolation.class); 075 076 private static final long serialVersionUID = 1L; 077 078 private final Schema schema; 079 080 private final List<PathNode> path; 081 082 private final Constraint constraint; 083 084 private final Object invalidValue; 085 086 public ConstraintViolation(Schema schema, List<PathNode> fieldPath, Constraint constraint, Object invalidValue) { 087 this.schema = schema; 088 path = new ArrayList<>(fieldPath); 089 this.constraint = constraint; 090 this.invalidValue = invalidValue; 091 } 092 093 public Schema getSchema() { 094 return schema; 095 } 096 097 public List<PathNode> getPath() { 098 return Collections.unmodifiableList(path); 099 } 100 101 public Constraint getConstraint() { 102 return constraint; 103 } 104 105 public Object getInvalidValue() { 106 return invalidValue; 107 } 108 109 /** 110 * @return The message if it's found in message bundles, a generic message otherwise. 111 * @since 7.1 112 */ 113 public String getMessage(Locale locale) { 114 // test whether there's a specific translation for for this field and this constraint 115 // the expected key is label.schema.constraint.violation.[constraintName].[schemaName].[field].[subField] 116 List<String> pathTokens = new ArrayList<>(); 117 pathTokens.add(Constraint.MESSAGES_KEY); 118 pathTokens.add(constraint.getDescription().getName()); 119 pathTokens.add(schema.getName()); 120 for (PathNode node : path) { 121 String name = node.getField().getName().getLocalName(); 122 pathTokens.add(name); 123 } 124 String key = StringUtils.join(pathTokens, '.'); 125 String computedInvalidValue = "null"; 126 if (invalidValue != null) { 127 String invalidValueString = invalidValue.toString(); 128 if (invalidValueString.length() > 20) { 129 computedInvalidValue = invalidValueString.substring(0, 15) + "..."; 130 } else { 131 computedInvalidValue = invalidValueString; 132 } 133 } 134 Object[] params = new Object[] { computedInvalidValue }; 135 Locale computedLocale = locale != null ? locale : Constraint.MESSAGES_DEFAULT_LANG; 136 String message; 137 try { 138 message = I18NUtils.getMessageString(Constraint.MESSAGES_BUNDLE, key, params, computedLocale); 139 } catch (MissingResourceException e) { 140 log.trace("No bundle found", e); 141 message = null; 142 } 143 if (message != null && !message.trim().isEmpty() && !key.equals(message)) { 144 // use the message if there's one 145 return message; 146 } else { 147 if (locale != null && Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { 148 // use the constraint message 149 return constraint.getErrorMessage(invalidValue, locale); 150 } else { 151 return getMessage(Locale.ENGLISH); 152 } 153 } 154 } 155 156 @Override 157 public String toString() { 158 return getMessage(Locale.ENGLISH); 159 } 160 161 /** 162 * Allows to locates some constraint violation in a document. 163 * <p> 164 * {@link #getIndex()} are used to indicates which element violates the constraint for list properties. 165 * </p> 166 * 167 * @since 7.1 168 */ 169 public static class PathNode { 170 171 private Field field; 172 173 private boolean listItem = false; 174 175 int index = 0; 176 177 public PathNode(Field field) { 178 this.field = field; 179 } 180 181 public PathNode(Field field, int index) { 182 super(); 183 this.field = field; 184 this.index = index; 185 listItem = true; 186 } 187 188 public Field getField() { 189 return field; 190 } 191 192 public int getIndex() { 193 return index; 194 } 195 196 public boolean isListItem() { 197 return listItem; 198 } 199 200 @Override 201 public String toString() { 202 if (listItem) { 203 return field.getName().getPrefixedName(); 204 } else { 205 return field.getName().getPrefixedName() + "[" + index + "]"; 206 } 207 } 208 209 } 210 211}