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.lang3.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}