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