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}