001/* 002 * (C) Copyright 2015 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 * Anahide Tchertchian 016 */ 017package org.nuxeo.ecm.platform.ui.web.validator; 018 019import java.util.LinkedHashSet; 020import java.util.List; 021import java.util.Locale; 022import java.util.Set; 023 024import javax.el.ValueExpression; 025import javax.el.ValueReference; 026import javax.faces.application.FacesMessage; 027import javax.faces.component.PartialStateHolder; 028import javax.faces.component.UIComponent; 029import javax.faces.context.FacesContext; 030import javax.faces.validator.Validator; 031import javax.faces.validator.ValidatorException; 032 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import org.nuxeo.ecm.core.api.DocumentModel; 036import org.nuxeo.ecm.core.api.model.Property; 037import org.nuxeo.ecm.core.api.validation.ConstraintViolation; 038import org.nuxeo.ecm.core.api.validation.DocumentValidationReport; 039import org.nuxeo.ecm.core.api.validation.DocumentValidationService; 040import org.nuxeo.ecm.core.schema.SchemaManager; 041import org.nuxeo.ecm.core.schema.types.Field; 042import org.nuxeo.ecm.platform.el.DocumentPropertyContext; 043import org.nuxeo.ecm.platform.ui.web.model.ProtectedEditableModel; 044import org.nuxeo.ecm.platform.ui.web.validator.ValueExpressionAnalyzer.ListItemMapper; 045import org.nuxeo.runtime.api.Framework; 046 047/** 048 * JSF validator for {@link DocumentModel} field constraints. 049 * 050 * @since 7.2 051 */ 052public class DocumentConstraintValidator implements Validator, PartialStateHolder { 053 054 private static final Log log = LogFactory.getLog(DocumentConstraintValidator.class); 055 056 public static final String VALIDATOR_ID = "DocumentConstraintValidator"; 057 058 private boolean transientValue = false; 059 060 private boolean initialState; 061 062 protected Boolean handleSubProperties; 063 064 @Override 065 public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { 066 if (context == null) { 067 throw new NullPointerException(); 068 } 069 if (component == null) { 070 throw new NullPointerException(); 071 } 072 ValueExpression ve = component.getValueExpression("value"); 073 if (ve == null) { 074 return; 075 } 076 077 ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(ve); 078 ValueReference vref = expressionAnalyzer.getReference(context.getELContext()); 079 080 if (log.isDebugEnabled()) { 081 log.debug(String.format("Validating value '%s' for expression '%s', base=%s, prop=%s", value, 082 ve.getExpressionString(), vref.getBase(), vref.getProperty())); 083 } 084 085 if (isResolvable(vref, ve)) { 086 List<ConstraintViolation> violations = doValidate(context, vref, ve, value); 087 if (violations != null && !violations.isEmpty()) { 088 Locale locale = context.getViewRoot().getLocale(); 089 if (violations.size() == 1) { 090 ConstraintViolation v = violations.iterator().next(); 091 String msg = v.getMessage(locale); 092 throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg)); 093 } else { 094 Set<FacesMessage> messages = new LinkedHashSet<FacesMessage>(violations.size()); 095 for (ConstraintViolation v : violations) { 096 String msg = v.getMessage(locale); 097 messages.add(new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg)); 098 } 099 throw new ValidatorException(messages); 100 } 101 } 102 } 103 } 104 105 @SuppressWarnings("rawtypes") 106 private boolean isResolvable(ValueReference ref, ValueExpression ve) { 107 if (ve == null || ref == null) { 108 return false; 109 } 110 Object base = ref.getBase(); 111 if (base != null) { 112 Class baseClass = base.getClass(); 113 if (baseClass != null) { 114 if (DocumentPropertyContext.class.isAssignableFrom(baseClass) 115 || (Property.class.isAssignableFrom(baseClass)) 116 || (ProtectedEditableModel.class.isAssignableFrom(baseClass)) 117 || (ListItemMapper.class.isAssignableFrom(baseClass))) { 118 return true; 119 } 120 } 121 } 122 if (log.isDebugEnabled()) { 123 log.debug(String.format("NOT validating %s, base=%s, prop=%s", ve.getExpressionString(), base, 124 ref.getProperty())); 125 } 126 return false; 127 } 128 129 protected List<ConstraintViolation> doValidate(FacesContext context, ValueReference vref, ValueExpression e, 130 Object value) { 131 DocumentValidationService s = Framework.getService(DocumentValidationService.class); 132 DocumentValidationReport report = null; 133 XPathAndField field = resolveField(context, vref, e); 134 if (field != null) { 135 boolean validateSubs = getHandleSubProperties().booleanValue(); 136 // use the xpath to validate the field 137 // this allow to get the custom message defined for field if there's error 138 report = s.validate(field.xpath, value, validateSubs); 139 if (log.isDebugEnabled()) { 140 log.debug(String.format("VALIDATED value '%s' for expression '%s', base=%s, prop=%s", value, 141 e.getExpressionString(), vref.getBase(), vref.getProperty())); 142 } 143 } else { 144 if (log.isDebugEnabled()) { 145 log.debug(String.format("NOT Validating value '%s' for expression '%s', base=%s, prop=%s", value, 146 e.getExpressionString(), vref.getBase(), vref.getProperty())); 147 } 148 } 149 150 if (report != null && report.hasError()) { 151 return report.asList(); 152 } 153 154 return null; 155 } 156 157 private class XPathAndField { 158 159 private Field field; 160 161 private String xpath; 162 163 public XPathAndField(Field field, String xpath) { 164 super(); 165 this.field = field; 166 this.xpath = xpath; 167 } 168 169 } 170 171 protected XPathAndField resolveField(FacesContext context, ValueReference vref, ValueExpression ve) { 172 Object base = vref.getBase(); 173 Object propObj = vref.getProperty(); 174 if (propObj != null && !(propObj instanceof String)) { 175 // ignore cases where prop would not be a String 176 return null; 177 } 178 String xpath = null; 179 Field field = null; 180 String prop = (String) propObj; 181 Class<?> baseClass = base.getClass(); 182 if (DocumentPropertyContext.class.isAssignableFrom(baseClass)) { 183 DocumentPropertyContext dc = (DocumentPropertyContext) base; 184 xpath = String.format("%s:%s", dc.getSchema(), prop); 185 field = getField(xpath); 186 } else if (Property.class.isAssignableFrom(baseClass)) { 187 xpath = ((Property) base).getPath() + "/" + prop; 188 field = getField(((Property) base).getField(), prop); 189 } else if (ProtectedEditableModel.class.isAssignableFrom(baseClass)) { 190 ProtectedEditableModel model = (ProtectedEditableModel) base; 191 ValueExpression listVe = model.getBinding(); 192 ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(listVe); 193 ValueReference listRef = expressionAnalyzer.getReference(context.getELContext()); 194 if (isResolvable(listRef, listVe)) { 195 XPathAndField parentField = resolveField(context, listRef, listVe); 196 if (parentField != null) { 197 field = getField(parentField.field, "*"); 198 if (parentField.xpath == null) { 199 xpath = field.getName().getLocalName(); 200 } else { 201 xpath = parentField.xpath + "/" + field.getName().getLocalName(); 202 } 203 } 204 } 205 } else if (ListItemMapper.class.isAssignableFrom(baseClass)) { 206 ListItemMapper mapper = (ListItemMapper) base; 207 ProtectedEditableModel model = mapper.getModel(); 208 ValueExpression listVe; 209 if (model.getParent() != null) { 210 // move one level up to resolve parent list binding 211 listVe = model.getParent().getBinding(); 212 } else { 213 listVe = model.getBinding(); 214 } 215 ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(listVe); 216 ValueReference listRef = expressionAnalyzer.getReference(context.getELContext()); 217 if (isResolvable(listRef, listVe)) { 218 XPathAndField parentField = resolveField(context, listRef, listVe); 219 if (parentField != null) { 220 field = getField(parentField.field, prop); 221 if (parentField.xpath == null) { 222 xpath = field.getName().getLocalName(); 223 } else { 224 xpath = parentField.xpath + "/" + field.getName().getLocalName(); 225 } 226 } 227 } 228 } else { 229 log.error(String.format("Cannot validate expression '%s, base=%s'", ve.getExpressionString(), base)); 230 } 231 // cleanup / on begin or at end 232 if (xpath != null) { 233 xpath = xpath.replaceAll("(^\\/)|(\\/$)", ""); 234 } 235 return new XPathAndField(field, xpath.replaceAll("(^\\\\/)|(\\\\/$)", "")); 236 } 237 238 protected Field getField(Field field, String subName) { 239 SchemaManager tm = Framework.getService(SchemaManager.class); 240 return tm.getField(field, subName); 241 } 242 243 protected Field getField(String propertyName) { 244 SchemaManager tm = Framework.getService(SchemaManager.class); 245 return tm.getField(propertyName); 246 } 247 248 public Boolean getHandleSubProperties() { 249 return handleSubProperties != null ? handleSubProperties : Boolean.TRUE; 250 } 251 252 public void setHandleSubProperties(Boolean handleSubProperties) { 253 clearInitialState(); 254 this.handleSubProperties = handleSubProperties; 255 } 256 257 @Override 258 public Object saveState(FacesContext context) { 259 if (context == null) { 260 throw new NullPointerException(); 261 } 262 if (!initialStateMarked()) { 263 Object values[] = new Object[1]; 264 values[0] = handleSubProperties; 265 return (values); 266 } 267 return null; 268 } 269 270 @Override 271 public void restoreState(FacesContext context, Object state) { 272 if (context == null) { 273 throw new NullPointerException(); 274 } 275 if (state != null) { 276 Object values[] = (Object[]) state; 277 handleSubProperties = (Boolean) values[0]; 278 } 279 } 280 281 @Override 282 public boolean isTransient() { 283 return transientValue; 284 } 285 286 @Override 287 public void setTransient(boolean newTransientValue) { 288 this.transientValue = newTransientValue; 289 } 290 291 @Override 292 public void markInitialState() { 293 initialState = true; 294 } 295 296 @Override 297 public boolean initialStateMarked() { 298 return initialState; 299 } 300 301 @Override 302 public void clearInitialState() { 303 initialState = false; 304 } 305 306}