/*
* (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Anahide Tchertchian
*/
package org.nuxeo.ecm.platform.ui.web.validator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.el.ValueExpression;
import javax.el.ValueReference;
import javax.faces.application.FacesMessage;
import javax.faces.component.PartialStateHolder;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.model.Property;
import org.nuxeo.ecm.core.api.validation.ConstraintViolation;
import org.nuxeo.ecm.core.api.validation.DocumentValidationReport;
import org.nuxeo.ecm.core.api.validation.DocumentValidationService;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.core.schema.types.Field;
import org.nuxeo.ecm.platform.el.DocumentPropertyContext;
import org.nuxeo.ecm.platform.ui.web.model.ProtectedEditableModel;
import org.nuxeo.ecm.platform.ui.web.validator.ValueExpressionAnalyzer.ListItemMapper;
import org.nuxeo.runtime.api.Framework;
/**
* JSF validator for {@link DocumentModel} field constraints.
*
* @since 7.2
*/
public class DocumentConstraintValidator implements Validator, PartialStateHolder {
private static final Log log = LogFactory.getLog(DocumentConstraintValidator.class);
public static final String VALIDATOR_ID = "DocumentConstraintValidator";
/**
* @since 8.4
*/
public static final String CTX_JSFVALIDATOR = "jsfValidator";
private boolean transientValue = false;
private boolean initialState;
protected Boolean handleSubProperties;
@Override
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
if (context == null) {
throw new NullPointerException();
}
if (component == null) {
throw new NullPointerException();
}
ValueExpression ve = component.getValueExpression("value");
if (ve == null) {
return;
}
ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(ve);
ValueReference vref = expressionAnalyzer.getReference(context.getELContext());
if (log.isDebugEnabled()) {
log.debug(String.format("Validating value '%s' for expression '%s', base=%s, prop=%s", value,
ve.getExpressionString(), vref.getBase(), vref.getProperty()));
}
if (isResolvable(vref, ve)) {
List<ConstraintViolation> violations = doValidate(context, vref, ve, value);
if (violations != null && !violations.isEmpty()) {
Locale locale = context.getViewRoot().getLocale();
if (violations.size() == 1) {
ConstraintViolation v = violations.iterator().next();
String msg = v.getMessage(locale);
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg));
} else {
Set<FacesMessage> messages = new LinkedHashSet<FacesMessage>(violations.size());
for (ConstraintViolation v : violations) {
String msg = v.getMessage(locale);
messages.add(new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg));
}
throw new ValidatorException(messages);
}
}
}
}
@SuppressWarnings("rawtypes")
private boolean isResolvable(ValueReference ref, ValueExpression ve) {
if (ve == null || ref == null) {
return false;
}
Object base = ref.getBase();
if (base != null) {
Class baseClass = base.getClass();
if (baseClass != null) {
if (DocumentPropertyContext.class.isAssignableFrom(baseClass)
|| (Property.class.isAssignableFrom(baseClass))
|| (ProtectedEditableModel.class.isAssignableFrom(baseClass))
|| (ListItemMapper.class.isAssignableFrom(baseClass))) {
return true;
}
}
}
if (log.isDebugEnabled()) {
log.debug(String.format("NOT validating %s, base=%s, prop=%s", ve.getExpressionString(), base,
ref.getProperty()));
}
return false;
}
protected List<ConstraintViolation> doValidate(FacesContext context, ValueReference vref, ValueExpression e,
Object value) {
DocumentValidationService validationService = Framework.getService(DocumentValidationService.class);
// document validation
DocumentValidationReport report = null;
if (!validationService.isActivated(CTX_JSFVALIDATOR, null)) {
return null;
}
XPathAndField field = resolveField(context, vref, e);
if (field != null) {
boolean validateSubs = getHandleSubProperties().booleanValue();
// use the xpath to validate the field
// this allow to get the custom message defined for field if there's error
report = validationService.validate(field.xpath, value, validateSubs);
if (log.isDebugEnabled()) {
log.debug(String.format("VALIDATED value '%s' for expression '%s', base=%s, prop=%s", value,
e.getExpressionString(), vref.getBase(), vref.getProperty()));
}
} else {
if (log.isDebugEnabled()) {
log.debug(String.format("NOT Validating value '%s' for expression '%s', base=%s, prop=%s", value,
e.getExpressionString(), vref.getBase(), vref.getProperty()));
}
}
if (report != null && report.hasError()) {
return report.asList();
}
return null;
}
private class XPathAndField {
private Field field;
private String xpath;
public XPathAndField(Field field, String xpath) {
super();
this.field = field;
this.xpath = xpath;
}
}
protected XPathAndField resolveField(FacesContext context, ValueReference vref, ValueExpression ve) {
Object base = vref.getBase();
Object propObj = vref.getProperty();
if (propObj != null && !(propObj instanceof String)) {
// ignore cases where prop would not be a String
return null;
}
String xpath = null;
Field field = null;
String prop = (String) propObj;
Class<?> baseClass = base.getClass();
if (DocumentPropertyContext.class.isAssignableFrom(baseClass)) {
DocumentPropertyContext dc = (DocumentPropertyContext) base;
xpath = dc.getSchema() + ":" + prop;
field = getField(xpath);
} else if (Property.class.isAssignableFrom(baseClass)) {
xpath = ((Property) base).getXPath() + "/" + prop;
field = getField(((Property) base).getField(), prop);
} else if (ProtectedEditableModel.class.isAssignableFrom(baseClass)) {
ProtectedEditableModel model = (ProtectedEditableModel) base;
ValueExpression listVe = model.getBinding();
ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(listVe);
ValueReference listRef = expressionAnalyzer.getReference(context.getELContext());
if (isResolvable(listRef, listVe)) {
XPathAndField parentField = resolveField(context, listRef, listVe);
if (parentField != null) {
field = getField(parentField.field, "*");
if (parentField.xpath == null) {
xpath = field.getName().getLocalName();
} else {
xpath = parentField.xpath + "/" + field.getName().getLocalName();
}
}
}
} else if (ListItemMapper.class.isAssignableFrom(baseClass)) {
ListItemMapper mapper = (ListItemMapper) base;
ProtectedEditableModel model = mapper.getModel();
ValueExpression listVe;
if (model.getParent() != null) {
// move one level up to resolve parent list binding
listVe = model.getParent().getBinding();
} else {
listVe = model.getBinding();
}
ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(listVe);
ValueReference listRef = expressionAnalyzer.getReference(context.getELContext());
if (isResolvable(listRef, listVe)) {
XPathAndField parentField = resolveField(context, listRef, listVe);
if (parentField != null) {
field = getField(parentField.field, prop);
if (field == null || field.getName() == null) {
// it should not happen but still, just in case
return null;
}
if (parentField.xpath == null) {
xpath = field.getName().getLocalName();
} else {
xpath = parentField.xpath + "/" + field.getName().getLocalName();
}
}
}
} else {
log.error(String.format("Cannot validate expression '%s, base=%s'", ve.getExpressionString(), base));
}
// cleanup / on begin or at end
if (xpath != null) {
xpath = StringUtils.strip(xpath, "/");
} else if (field == null && xpath == null) {
return null;
}
return new XPathAndField(field, xpath);
}
protected Field getField(Field field, String subName) {
SchemaManager tm = Framework.getService(SchemaManager.class);
return tm.getField(field, subName);
}
protected Field getField(String propertyName) {
SchemaManager tm = Framework.getService(SchemaManager.class);
return tm.getField(propertyName);
}
public Boolean getHandleSubProperties() {
return handleSubProperties != null ? handleSubProperties : Boolean.TRUE;
}
public void setHandleSubProperties(Boolean handleSubProperties) {
clearInitialState();
this.handleSubProperties = handleSubProperties;
}
@Override
public Object saveState(FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
if (!initialStateMarked()) {
Object values[] = new Object[1];
values[0] = handleSubProperties;
return (values);
}
return null;
}
@Override
public void restoreState(FacesContext context, Object state) {
if (context == null) {
throw new NullPointerException();
}
if (state != null) {
Object values[] = (Object[]) state;
handleSubProperties = (Boolean) values[0];
}
}
@Override
public boolean isTransient() {
return transientValue;
}
@Override
public void setTransient(boolean newTransientValue) {
this.transientValue = newTransientValue;
}
@Override
public void markInitialState() {
initialState = true;
}
@Override
public boolean initialStateMarked() {
return initialState;
}
@Override
public void clearInitialState() {
initialState = false;
}
}