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