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