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 <nchapurlat@nuxeo.com> 018 */ 019 020package org.nuxeo.ecm.core.api.validation; 021 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.List; 025import java.util.Locale; 026import java.util.MissingResourceException; 027import java.util.stream.Collectors; 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 Constraint#MESSAGES_KEY} key in {@value Constraint#MESSAGES_BUNDLE} bundle to customize default 043 * message</li> 044 * <li>Append the constraint name to the previous key to customize the generic message to some constraint</li> 045 * <li>Append the schema and the field name to the previous key to customize the message for a specific constraint 046 * applied to some specific schema field.</li> 047 * </ul> 048 * <br> 049 * For each messages, you can use parameters in the message : 050 * <ul> 051 * <li>The invalid value : {0}</li> 052 * <li>The schema name : {1}</li> 053 * <li>The field name : {2}</li> 054 * <li>The constraint name : {3}</li> 055 * <li>The first constraint parameter (if exists) : {4}</li> 056 * <li>The second constraint parameter (if exists) : {5}</li> 057 * <li>...</li> 058 * </ul> 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 * 069 * @since 7.1 070 */ 071public class ConstraintViolation implements ValidationViolation { 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<>(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 String getPathAsString() { 101 return path.stream().map(PathNode::toString).collect(Collectors.joining("/")); 102 } 103 104 public Constraint getConstraint() { 105 return constraint; 106 } 107 108 public Object getInvalidValue() { 109 return invalidValue; 110 } 111 112 /** 113 * @return The message if it's found in message bundles, a generic message otherwise. 114 * @since 7.1 115 */ 116 @Override 117 public String getMessage(Locale locale) { 118 String key = getMessageKey(); 119 String computedInvalidValue = "null"; 120 if (invalidValue != null) { 121 String invalidValueString = invalidValue.toString(); 122 if (invalidValueString.length() > 20) { 123 computedInvalidValue = invalidValueString.substring(0, 15) + "..."; 124 } else { 125 computedInvalidValue = invalidValueString; 126 } 127 } 128 Object[] params = new Object[] { computedInvalidValue }; 129 Locale computedLocale = locale != null ? locale : Constraint.MESSAGES_DEFAULT_LANG; 130 String message; 131 try { 132 message = I18NUtils.getMessageString(Constraint.MESSAGES_BUNDLE, key, params, computedLocale); 133 } catch (MissingResourceException e) { 134 log.trace("No bundle found", e); 135 message = null; 136 } 137 if (message != null && !message.trim().isEmpty() && !key.equals(message)) { 138 // use the message if there's one 139 return message; 140 } else { 141 if (locale != null && Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) { 142 // use the constraint message 143 return constraint.getErrorMessage(invalidValue, locale); 144 } else { 145 return getMessage(Locale.ENGLISH); 146 } 147 } 148 } 149 150 @Override 151 public String toString() { 152 return getMessage(Locale.ENGLISH); 153 } 154 155 /** 156 * Allows to locates some constraint violation in a document. 157 * <p> 158 * {@link #getIndex()} are used to indicates which element violates the constraint for list properties. 159 * </p> 160 * 161 * @since 7.1 162 */ 163 public static class PathNode { 164 165 private Field field; 166 167 private boolean listItem = false; 168 169 int index = 0; 170 171 public PathNode(Field field) { 172 this.field = field; 173 } 174 175 public PathNode(Field field, int index) { 176 super(); 177 this.field = field; 178 this.index = index; 179 listItem = true; 180 } 181 182 public Field getField() { 183 return field; 184 } 185 186 public int getIndex() { 187 return index; 188 } 189 190 public boolean isListItem() { 191 return listItem; 192 } 193 194 @Override 195 public String toString() { 196 if (listItem) { 197 return field.getName().getPrefixedName(); 198 } else { 199 return field.getName().getPrefixedName() + "[" + index + "]"; 200 } 201 } 202 203 } 204 205 @Override 206 public String getMessageKey() { 207 // test whether there's a specific translation for this field and this constraint 208 // the expected key is label.schema.constraint.violation.[constraintName].[schemaName].[field].[subField] 209 List<String> pathTokens = new ArrayList<>(); 210 pathTokens.add(constraint.getMessageKey()); 211 pathTokens.add(schema.getName()); 212 for (PathNode node : path) { 213 String name = node.getField().getName().getLocalName(); 214 pathTokens.add(name); 215 } 216 return StringUtils.join(pathTokens, '.'); 217 } 218 219}