001/*
002 * (C) Copyright 2006-2013 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 *     Bogdan Stefanescu
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.core.event.impl;
021
022import java.io.IOException;
023import java.net.URL;
024import java.util.HashSet;
025import java.util.Set;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.nuxeo.common.xmap.annotation.XNode;
030import org.nuxeo.common.xmap.annotation.XNodeList;
031import org.nuxeo.common.xmap.annotation.XObject;
032import org.nuxeo.ecm.core.api.NuxeoException;
033import org.nuxeo.ecm.core.event.Event;
034import org.nuxeo.ecm.core.event.EventBundle;
035import org.nuxeo.ecm.core.event.EventListener;
036import org.nuxeo.ecm.core.event.PostCommitEventListener;
037import org.nuxeo.ecm.core.event.PostCommitFilteringEventListener;
038import org.nuxeo.ecm.core.event.script.Script;
039import org.nuxeo.ecm.core.event.script.ScriptingEventListener;
040import org.nuxeo.ecm.core.event.script.ScriptingPostCommitEventListener;
041import org.nuxeo.runtime.model.RuntimeContext;
042
043/**
044 * XObject descriptor to declare event listeners
045 */
046@XObject("listener")
047public class EventListenerDescriptor {
048
049    public static final Log log = LogFactory.getLog(EventListenerDescriptor.class);
050
051    @XNode("@name")
052    protected String name;
053
054    /**
055     * The event listener class.
056     */
057    @XNode("@class")
058    protected String className;
059
060    /**
061     * A script reference: URL, file path, or bundle entry. Runtime variable are expanded. To specify a bundle entry use
062     * the URL schema "bundle:"
063     */
064    @XNode("@script")
065    protected String script;
066
067    /**
068     * Applies only for scripts.
069     */
070    @XNode("@postCommit")
071    protected boolean isPostCommit;
072
073    /**
074     * Applies only for post commit listener
075     */
076    @XNode("@async")
077    protected Boolean isAsync;
078
079    @XNode("@transactionTimeOut")
080    protected Integer transactionTimeOut;
081
082    /**
083     * The priority to be used to order listeners.
084     */
085    @XNode("@priority")
086    protected Integer priority;
087
088    @XNode("@enabled")
089    protected boolean isEnabled = true;
090
091    @XNode("@retryCount")
092    protected Integer retryCount;
093
094    @XNode("@singlethread")
095    protected boolean singleThreaded = false;
096
097    protected Set<String> events;
098
099    protected RuntimeContext rc;
100
101    protected EventListener inLineListener;
102
103    protected PostCommitEventListener postCommitEventListener;
104
105    public int getPriority() {
106        return priority == null ? 0 : priority.intValue();
107    }
108
109    public void setRuntimeContext(RuntimeContext rc) {
110        this.rc = rc;
111    }
112
113    public RuntimeContext getRuntimeContext() {
114        return rc;
115    }
116
117    public boolean isEnabled() {
118        return isEnabled;
119    }
120
121    public Integer getRetryCount() {
122        return retryCount;
123    }
124
125    public Set<String> getEvents() {
126        return events;
127    }
128
129    @XNodeList(value = "event", componentType = String.class, type = HashSet.class, nullByDefault = true)
130    public void setEvents(Set<String> events) {
131        this.events = events.isEmpty() ? null : events;
132    }
133
134    public void setEnabled(boolean isEnabled) {
135        this.isEnabled = isEnabled;
136    }
137
138    public void setRetryCount(Integer retryCount) {
139        this.retryCount = retryCount;
140    }
141
142    public void initListener() {
143        try {
144            if (className != null) {
145                Class<?> klass = getRuntimeContext().loadClass(className);
146                if (EventListener.class.isAssignableFrom(klass)) {
147                    inLineListener = (EventListener) klass.getDeclaredConstructor().newInstance();
148                    isPostCommit = false;
149                } else if (PostCommitEventListener.class.isAssignableFrom(klass)) {
150                    postCommitEventListener = (PostCommitEventListener) klass.getDeclaredConstructor().newInstance();
151                    isPostCommit = true;
152                } else {
153                    throw new IllegalArgumentException(
154                            "Listener extension must define a class extending EventListener or PostCommitEventListener: '"
155                                    + className + "'.");
156                }
157            } else if (script != null) {
158                if (isPostCommit) {
159                    postCommitEventListener = new ScriptingPostCommitEventListener(getScript());
160                } else {
161                    inLineListener = new ScriptingEventListener(getScript());
162                }
163            } else {
164                throw new IllegalArgumentException("Listener extension must define either a class or a script");
165            }
166        } catch (ReflectiveOperationException | NoClassDefFoundError | IOException e) {
167            throw new NuxeoException(e);
168        }
169    }
170
171    public EventListener asEventListener() {
172        return inLineListener;
173    }
174
175    public PostCommitEventListener asPostCommitListener() {
176        return postCommitEventListener;
177    }
178
179    public Script getScript() throws IOException {
180        if (rc != null) {
181            URL url = rc.getBundle().getEntry(script);
182            if (url == null) {
183                // if not found using bundle entries try using classloader
184                // in a test environment bundle entries may not work
185                url = rc.getResource(script);
186                if (url == null) {
187                    throw new IOException("Script Not found: " + script);
188                }
189            }
190            return Script.newScript(url);
191        } else {
192            return Script.newScript(script);
193        }
194    }
195
196    public String getName() {
197        if (name == null) {
198            if (className != null) {
199                name = className;
200            } else {
201                name = script;
202            }
203        }
204        return name;
205    }
206
207    public Integer getTransactionTimeout() {
208        return transactionTimeOut;
209    }
210
211    public void merge(EventListenerDescriptor other) {
212
213        isEnabled = other.isEnabled;
214
215        if (other.className != null) {
216            className = other.className;
217            rc = other.rc;
218        } else if (other.script != null) {
219            script = other.script;
220            className = null;
221            rc = other.rc;
222        }
223
224        if (other.isAsync != null) {
225            isAsync = other.isAsync;
226        }
227
228        if (other.events != null) {
229            events = other.events;
230        }
231
232        if (other.transactionTimeOut != null) {
233            transactionTimeOut = other.transactionTimeOut;
234        }
235
236        if (other.priority != null) {
237            priority = other.priority;
238        }
239
240        if (other.retryCount != null) {
241            retryCount = other.retryCount;
242        }
243    }
244
245    public final boolean acceptEvent(String eventName) {
246        return events == null || events.contains(eventName);
247    }
248
249    public void setIsAsync(Boolean isAsync) {
250        this.isAsync = isAsync;
251    }
252
253    public boolean getIsAsync() {
254        return Boolean.TRUE.equals(isAsync);
255    }
256
257    public boolean isSingleThreaded() {
258        return singleThreaded;
259    }
260
261    /**
262     * Filters the event bundle to only keep events of interest to this listener.
263     *
264     * @since 5.7
265     */
266    public EventBundle filterBundle(EventBundle bundle) {
267        EventBundle filtered = new EventBundleImpl();
268
269        for (Event event : bundle) {
270            if (!acceptEvent(event.getName())) {
271                continue;
272            }
273            PostCommitEventListener pcl = asPostCommitListener();
274            if (pcl instanceof PostCommitFilteringEventListener
275                    && !((PostCommitFilteringEventListener) pcl).acceptEvent(event)) {
276                continue;
277            }
278            filtered.push(event);
279        }
280        return filtered;
281    }
282
283    /**
284     * Checks if there's at least one event of interest in the bundle.
285     *
286     * @since 5.7
287     */
288    public boolean acceptBundle(EventBundle bundle) {
289        for (Event event : bundle) {
290            if (!acceptEvent(event.getName())) {
291                continue;
292            }
293            PostCommitEventListener pcl = asPostCommitListener();
294            if (pcl instanceof PostCommitFilteringEventListener
295                    && !((PostCommitFilteringEventListener) pcl).acceptEvent(event)) {
296                continue;
297            }
298            return true;
299        }
300        return false;
301    }
302
303    @Override
304    public String toString() {
305        return "EventListenerDescriptor [name=" + name + ", className=" + className + ", isPostCommit=" + isPostCommit
306                + ", isAsync=" + isAsync + "]";
307    }
308
309}