001/*
002 * (C) Copyright 2006-2011 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 *     bstefanescu
018 */
019package org.nuxeo.ecm.automation.core.events;
020
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Set;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.mvel2.CompileException;
030import org.nuxeo.common.utils.StringUtils;
031import org.nuxeo.common.xmap.annotation.XNode;
032import org.nuxeo.common.xmap.annotation.XNodeList;
033import org.nuxeo.common.xmap.annotation.XObject;
034import org.nuxeo.ecm.automation.OperationContext;
035import org.nuxeo.ecm.automation.core.scripting.Expression;
036import org.nuxeo.ecm.automation.core.scripting.Scripting;
037import org.nuxeo.ecm.core.api.DocumentModel;
038import org.nuxeo.ecm.core.api.Filter;
039import org.nuxeo.ecm.core.api.NuxeoPrincipal;
040import org.nuxeo.ecm.core.event.EventContext;
041
042/**
043 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
044 */
045@XObject("handler")
046public class EventHandler {
047
048    private static final Log log = LogFactory.getLog(EventHandler.class);
049
050    @XNode("@chainId")
051    protected String chainId;
052
053    @XNode("@postCommit")
054    protected boolean isPostCommit;
055
056    @XNodeList(value = "event", type = HashSet.class, componentType = String.class)
057    protected Set<String> events;
058
059    @XNodeList(value = "filters/doctype", type = HashSet.class, componentType = String.class, nullByDefault = true)
060    protected Set<String> doctypes;
061
062    @XNode("filters/facet")
063    protected String facet;
064
065    @XNode("filters/lifeCycle")
066    protected void setLifeCycleExpr(String lifeCycles) {
067        lifeCycle = StringUtils.split(lifeCycles, ',', true);
068    }
069
070    protected String[] lifeCycle;
071
072    @XNode("filters/pathStartsWith")
073    protected String pathStartsWith;
074
075    protected Filter attribute;
076
077    @XNode("filters/attribute")
078    public void setAttribute(String attribute) {
079        this.attribute = DocumentAttributeFilterFactory.getFilter(attribute);
080    }
081
082    /**
083     * the principal should be member of at least one of the groups. OR is used
084     */
085    @XNodeList(value = "filters/group", type = ArrayList.class, componentType = String.class)
086    protected List<String> memberOf;
087
088    @XNode("filters/isAdministrator")
089    protected Boolean isAdministrator;
090
091    /**
092     * @deprecated since 5.7: expression evaluation was inverted so a new attribute condition has been defined, see
093     *             NXP-8630
094     */
095    @Deprecated
096    protected String expression;
097
098    /**
099     * @since 5.7: added to replace the 'expression' element as its evaluation is inverted
100     */
101    protected String condition;
102
103    @XNode("filters/expression")
104    protected void _setExpression(String expr) {
105        expression = convertExpr(expr);
106    }
107
108    @XNode("filters/condition")
109    protected void _setCondition(String expr) {
110        condition = convertExpr(expr);
111    }
112
113    protected String convertExpr(String expr) {
114        String res = expr.replaceAll("&lt;", "<");
115        res = res.replaceAll("&gt;", ">");
116        res = res.replaceAll("&amp;", "&");
117        return res;
118    }
119
120    public EventHandler() {
121    }
122
123    public EventHandler(String eventId, String chainId) {
124        this(Collections.singleton(eventId), chainId);
125    }
126
127    public EventHandler(Set<String> eventId, String chainId) {
128        events = eventId;
129        this.chainId = chainId;
130    }
131
132    public Set<String> getEvents() {
133        return events;
134    }
135
136    public String getChainId() {
137        return chainId;
138    }
139
140    public void setPostCommit(boolean isPostCommit) {
141        this.isPostCommit = isPostCommit;
142    }
143
144    public boolean isPostCommit() {
145        return isPostCommit;
146    }
147
148    public void setAttributeFilter(Filter attribute) {
149        this.attribute = attribute;
150    }
151
152    public void setIsAdministrator(Boolean isAdministrator) {
153        this.isAdministrator = isAdministrator;
154    }
155
156    public void setMemberOf(List<String> groups) {
157        memberOf = groups;
158    }
159
160    public void setPathStartsWith(String pathStartsWith) {
161        this.pathStartsWith = pathStartsWith;
162    }
163
164    /**
165     * @deprecated since 5.7.1 use {@link #setCondition(String)} instead
166     */
167    @Deprecated
168    public void setExpression(String expression) {
169        this.expression = expression;
170    }
171
172    public void setDoctypes(Set<String> doctypes) {
173        this.doctypes = doctypes;
174    }
175
176    public void setFacet(String facet) {
177        this.facet = facet;
178    }
179
180    public void setLifeCycle(String[] lifeCycle) {
181        this.lifeCycle = lifeCycle;
182    }
183
184    public void setChainId(String chainId) {
185        this.chainId = chainId;
186    }
187
188    /**
189     * @deprecated since 5.7: use {@link #getCondition()} instead
190     */
191    @Deprecated
192    public String getExpression() {
193        return expression;
194    }
195
196    /**
197     * Condition to define on event handler
198     *
199     * @since 5.7
200     */
201    public String getCondition() {
202        return condition;
203    }
204
205    /**
206     * @since 5.9.1
207     */
208    public void setCondition(String condition) {
209        this.condition = condition;
210    }
211
212    public String getFacet() {
213        return facet;
214    }
215
216    public Filter getAttribute() {
217        return attribute;
218    }
219
220    public String[] getLifeCycle() {
221        return lifeCycle;
222    }
223
224    public List<String> getMemberOf() {
225        return memberOf;
226    }
227
228    public Boolean getIsAdministrator() {
229        return isAdministrator;
230    }
231
232    public String getPathStartsWith() {
233        return pathStartsWith;
234    }
235
236    public Set<String> getDoctypes() {
237        return doctypes;
238    }
239
240    /**
241     * @deprecated since 5.7: use
242     */
243    @Deprecated
244    public Expression getExpr() {
245        return Scripting.newExpression(expression);
246    }
247
248    /**
249     * Checks if this handler should run for the event and operation context.
250     *
251     * @param quick If {@code true}, then this method may not check all filter parameters like {@code filter/expression}
252     *            and just return {@code true} to avoid costly evaluations on {@link ShallowDocumentModel} instances
253     */
254    public boolean isEnabled(OperationContext ctx, EventContext eventCtx, boolean quick) {
255        Object obj = ctx.getInput();
256        DocumentModel doc = null;
257        if (obj instanceof DocumentModel) {
258            doc = (DocumentModel) obj;
259        }
260        if (doctypes != null) {
261            if (doc == null || (!doctypes.isEmpty() && !doctypes.contains(doc.getType()))) {
262                return false;
263            }
264        }
265        if (facet != null) {
266            if (doc == null || !doc.hasFacet(facet)) {
267                return false;
268            }
269        }
270        if (lifeCycle != null && lifeCycle.length > 0) {
271            if (doc == null) {
272                return false;
273            }
274            boolean match = false;
275            String currentLc = doc.getCurrentLifeCycleState();
276            for (String lc : lifeCycle) {
277                if (lc.equals(currentLc)) {
278                    match = true;
279                    break;
280                }
281            }
282            if (!match) {
283                return false;
284            }
285        }
286        if (attribute != null) {
287            if (doc == null || !attribute.accept(doc)) {
288                return false;
289            }
290        }
291        if (pathStartsWith != null) {
292            if (doc == null || !doc.getPathAsString().startsWith(pathStartsWith)) {
293                return false;
294            }
295        }
296        if (memberOf != null && !memberOf.isEmpty()) {
297            NuxeoPrincipal p = (NuxeoPrincipal) eventCtx.getPrincipal();
298            boolean granted = false;
299            for (String group : memberOf) {
300                if (p.isMemberOf(group)) {
301                    granted = true;
302                    break;
303                }
304            }
305            if (!granted) {
306                return false;
307            }
308        }
309        if (isAdministrator != null) {
310            if (!((NuxeoPrincipal) eventCtx.getPrincipal()).isAdministrator()) {
311                return false;
312            }
313        }
314        if (quick) {
315            return true;
316        }
317        /*
318         * The following are not evaluated in quick mode, as we need a full DocumentModelImpl to evaluate most
319         * expressions.
320         */
321        if (!org.apache.commons.lang.StringUtils.isBlank(condition)) {
322            Expression expr = Scripting.newExpression(condition);
323            try {
324                if (!Boolean.TRUE.equals(expr.eval(ctx))) {
325                    return false;
326                }
327            } catch (CompileException e) {
328                // happens for expressions evaluated over a DeletedDocumentModel for instance
329                log.debug("Failed to execute expression: " + e, e);
330                return false;
331            }
332        } else if (!org.apache.commons.lang.StringUtils.isBlank(expression)) {
333            // BBB
334            if (log.isWarnEnabled()) {
335                log.warn(String.format("The 'expression' element with value '%s' "
336                        + "on event handler for chain '%s' is deprecated: please use the 'condition'"
337                        + " attribute instead, as its evaluation will not be inverted.", expression, chainId));
338            }
339            Expression expr = Scripting.newExpression(expression);
340            if (Boolean.TRUE.equals(expr.eval(ctx))) {
341                return false;
342            }
343        }
344        return true;
345    }
346}