001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.scxml.env;
018    
019    import java.io.Serializable;
020    import java.util.Collections;
021    import java.util.HashMap;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.Timer;
025    import java.util.TimerTask;
026    
027    import org.apache.commons.logging.Log;
028    import org.apache.commons.logging.LogFactory;
029    import org.apache.commons.scxml.EventDispatcher;
030    import org.apache.commons.scxml.SCXMLExecutor;
031    import org.apache.commons.scxml.SCXMLHelper;
032    import org.apache.commons.scxml.TriggerEvent;
033    import org.apache.commons.scxml.model.ModelException;
034    
035    /**
036     * <p>EventDispatcher implementation that can schedule <code>delay</code>ed
037     * <send> events for the "scxml" <code>targettype</code>
038     * attribute value (which is also the default). This implementation uses
039     * J2SE <code>Timer</code>s.</p>
040     *
041     * <p>No other <code>targettype</code>s are processed. Subclasses may support
042     * additional <code>targettype</code>s by overriding the
043     * <code>send(...)</code> and <code>cancel(...)</code> methods and
044     * delegating to their <code>super</code> counterparts for the
045     * "scxml" <code>targettype</code>.</p>
046     *
047     */
048    public class SimpleScheduler implements EventDispatcher, Serializable {
049    
050        /** Serial version UID. */
051        private static final long serialVersionUID = 1L;
052    
053        /** Log instance. */
054        private Log log = LogFactory.getLog(SimpleScheduler.class);
055    
056        /**
057         * The <code>Map</code> of active <code>Timer</code>s, keyed by
058         * <send> element <code>id</code>s.
059         */
060        private Map timers;
061    
062        /**
063         * The state chart execution instance we schedule events for.
064         */
065        private SCXMLExecutor executor;
066    
067        /**
068         * Constructor.
069         *
070         * @param executor The owning {@link SCXMLExecutor} instance.
071         */
072        public SimpleScheduler(final SCXMLExecutor executor) {
073            super();
074            this.executor = executor;
075            this.timers = Collections.synchronizedMap(new HashMap());
076        }
077    
078        /**
079         * @see EventDispatcher#cancel(String)
080         */
081        public void cancel(final String sendId) {
082            // Log callback
083            if (log.isInfoEnabled()) {
084                log.info("cancel( sendId: " + sendId + ")");
085            }
086            if (!timers.containsKey(sendId)) {
087                return; // done, we don't track this one or its already expired
088            }
089            Timer timer = (Timer) timers.get(sendId);
090            if (timer != null) {
091                timer.cancel();
092                if (log.isDebugEnabled()) {
093                    log.debug("Cancelled event scheduled by <send> with id '"
094                        + sendId + "'");
095                }
096            }
097            timers.remove(sendId);
098        }
099    
100        /**
101        @see EventDispatcher#send(String,String,String,String,Map,Object,long,List)
102         */
103        public void send(final String sendId, final String target,
104                final String targettype, final String event, final Map params,
105                final Object hints, final long delay, final List externalNodes) {
106            // Log callback
107            if (log.isInfoEnabled()) {
108                StringBuffer buf = new StringBuffer();
109                buf.append("send ( sendId: ").append(sendId);
110                buf.append(", target: ").append(target);
111                buf.append(", targetType: ").append(targettype);
112                buf.append(", event: ").append(event);
113                buf.append(", params: ").append(String.valueOf(params));
114                buf.append(", hints: ").append(String.valueOf(hints));
115                buf.append(", delay: ").append(delay);
116                buf.append(')');
117                log.info(buf.toString());
118            }
119    
120            // We only handle the "scxml" targettype (which is the default too)
121            if (SCXMLHelper.isStringEmpty(targettype)
122                    || targettype.trim().equalsIgnoreCase(TARGETTYPE_SCXML)) {
123    
124                if (!SCXMLHelper.isStringEmpty(target)) {
125                    // We know of no other target
126                    if (log.isWarnEnabled()) {
127                        log.warn("<send>: Unavailable target - " + target);
128                    }
129                    try {
130                        this.executor.triggerEvent(new TriggerEvent(
131                            EVENT_ERR_SEND_TARGETUNAVAILABLE,
132                            TriggerEvent.ERROR_EVENT));
133                    } catch (ModelException me) {
134                        log.error(me.getMessage(), me);
135                    }
136                    return; // done
137                }
138    
139                if (delay > 0L) {
140                    // Need to schedule this one
141                    Timer timer = new Timer(true);
142                    timer.schedule(new DelayedEventTask(sendId, event, params), delay);
143                    timers.put(sendId, timer);
144                    if (log.isDebugEnabled()) {
145                        log.debug("Scheduled event '" + event + "' with delay "
146                            + delay + "ms, as specified by <send> with id '"
147                            + sendId + "'");
148                    }
149                }
150                // else short-circuited by Send#execute()
151                // TODO: Pass through in v1.0
152    
153            }
154    
155        }
156    
157        /**
158         * Get the log instance.
159         *
160         * @return The current log instance
161         */
162        protected Log getLog() {
163            return log;
164        }
165    
166        /**
167         * Get the current timers.
168         *
169         * @return The currently scheduled timers
170         */
171        protected Map getTimers() {
172            return timers;
173        }
174    
175        /**
176         * Get the executor we're attached to.
177         *
178         * @return The owning executor instance
179         */
180        protected SCXMLExecutor getExecutor() {
181            return executor;
182        }
183    
184        /**
185         * TimerTask implementation.
186         */
187        class DelayedEventTask extends TimerTask {
188    
189            /**
190             * The ID of the <send> element.
191             */
192            private String sendId;
193    
194            /**
195             * The event name.
196             */
197            private String event;
198    
199            /**
200             * The event payload, if any.
201             */
202            private Map payload;
203    
204            /**
205             * Constructor.
206             *
207             * @param sendId The ID of the send element.
208             * @param event The name of the event to be triggered.
209             */
210            DelayedEventTask(final String sendId, final String event) {
211                this(sendId, event, null);
212            }
213    
214            /**
215             * Constructor for events with payload.
216             *
217             * @param sendId The ID of the send element.
218             * @param event The name of the event to be triggered.
219             * @param payload The event payload, if any.
220             */
221            DelayedEventTask(final String sendId, final String event,
222                    final Map payload) {
223                super();
224                this.sendId = sendId;
225                this.event = event;
226                this.payload = payload;
227            }
228    
229            /**
230             * What to do when timer expires.
231             */
232            public void run() {
233                try {
234                    executor.triggerEvent(new TriggerEvent(event,
235                        TriggerEvent.SIGNAL_EVENT, payload));
236                } catch (ModelException me) {
237                    log.error(me.getMessage(), me);
238                }
239                timers.remove(sendId);
240                if (log.isDebugEnabled()) {
241                    log.debug("Fired event '" + event + "' as scheduled by "
242                        + "<send> with id '" + sendId + "'");
243                }
244            }
245    
246        }
247    
248        /**
249         * The default targettype.
250         */
251        private static final String TARGETTYPE_SCXML = "scxml";
252    
253        /**
254         * The spec mandated derived event when target cannot be reached.
255         */
256        private static final String EVENT_ERR_SEND_TARGETUNAVAILABLE =
257            "error.send.targetunavailable";
258    
259    }
260