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.model;
018    
019    import java.util.ArrayList;
020    import java.util.Collection;
021    import java.util.HashMap;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.StringTokenizer;
025    
026    import org.apache.commons.logging.Log;
027    import org.apache.commons.scxml.Context;
028    import org.apache.commons.scxml.ErrorReporter;
029    import org.apache.commons.scxml.Evaluator;
030    import org.apache.commons.scxml.EventDispatcher;
031    import org.apache.commons.scxml.SCInstance;
032    import org.apache.commons.scxml.SCXMLExpressionException;
033    import org.apache.commons.scxml.SCXMLHelper;
034    import org.apache.commons.scxml.TriggerEvent;
035    import org.apache.commons.scxml.semantics.ErrorConstants;
036    
037    /**
038     * The class in this SCXML object model that corresponds to the
039     * <send> SCXML element.
040     *
041     */
042    public class Send extends Action implements ExternalContent {
043    
044        /**
045         * Serial version UID.
046         */
047        private static final long serialVersionUID = 1L;
048    
049        /**
050         * The default targettype.
051         */
052        private static final String TARGETTYPE_SCXML = "scxml";
053    
054        /**
055         * The spec mandated derived event when target cannot be reached
056         * for TARGETTYPE_SCXML.
057         */
058        private static final String EVENT_ERR_SEND_TARGETUNAVAILABLE =
059            "error.send.targetunavailable";
060    
061        /**
062         * The ID of the send message.
063         */
064        private String sendid;
065    
066        /**
067         * An expression returning the target location of the event.
068         */
069        private String target;
070    
071        /**
072         * The type of the Event I/O Processor that the event.
073         * should be dispatched to
074         */
075        private String targettype;
076    
077        /**
078         * The event is dispatched after the delay interval elapses.
079         */
080        private String delay;
081    
082        /**
083         * The data containing information which may be used by the
084         * implementing platform to configure the event processor.
085         */
086        private String hints;
087    
088        /**
089         * The namelist to the sent.
090         */
091        private String namelist;
092    
093        /**
094         * The list of external nodes associated with this <send> element.
095         */
096        private List externalNodes;
097    
098        /**
099         * The type of event being generated.
100         */
101        private String event;
102    
103        /**
104         * OutputFormat used to serialize external nodes.
105         *
106        private static final OutputFormat format;
107        static {
108            format = new OutputFormat();
109            format.setOmitXMLDeclaration(true);
110        }
111        */
112    
113        /**
114         * Constructor.
115         */
116        public Send() {
117            super();
118            this.externalNodes = new ArrayList();
119        }
120    
121        /**
122         * Get the delay.
123         *
124         * @return Returns the delay.
125         */
126        public final String getDelay() {
127            return delay;
128        }
129    
130        /**
131         * Set the delay.
132         *
133         * @param delay The delay to set.
134         */
135        public final void setDelay(final String delay) {
136            this.delay = delay;
137        }
138    
139        /**
140         * Get the list of external namespaced child nodes.
141         *
142         * @return List Returns the list of externalnodes.
143         */
144        public final List getExternalNodes() {
145            return externalNodes;
146        }
147    
148        /**
149         * Set the list of external namespaced child nodes.
150         *
151         * @param externalNodes The externalnode to set.
152         */
153        public final void setExternalNodes(final List externalNodes) {
154            this.externalNodes = externalNodes;
155        }
156    
157        /**
158         * Get the hints for this <send> element.
159         *
160         * @return String Returns the hints.
161         */
162        public final String getHints() {
163            return hints;
164        }
165    
166        /**
167         * Set the hints for this <send> element.
168         *
169         * @param hints The hints to set.
170         */
171        public final void setHints(final String hints) {
172            this.hints = hints;
173        }
174    
175        /**
176         * Get the namelist.
177         *
178         * @return String Returns the namelist.
179         */
180        public final String getNamelist() {
181            return namelist;
182        }
183    
184        /**
185         * Set the namelist.
186         *
187         * @param namelist The namelist to set.
188         */
189        public final void setNamelist(final String namelist) {
190            this.namelist = namelist;
191        }
192    
193        /**
194         * Get the identifier for this <send> element.
195         *
196         * @return String Returns the sendid.
197         */
198        public final String getSendid() {
199            return sendid;
200        }
201    
202        /**
203         * Set the identifier for this <send> element.
204         *
205         * @param sendid The sendid to set.
206         */
207        public final void setSendid(final String sendid) {
208            this.sendid = sendid;
209        }
210    
211        /**
212         * Get the target for this <send> element.
213         *
214         * @return String Returns the target.
215         */
216        public final String getTarget() {
217            return target;
218        }
219    
220        /**
221         * Set the target for this <send> element.
222         *
223         * @param target The target to set.
224         */
225        public final void setTarget(final String target) {
226            this.target = target;
227        }
228    
229        /**
230         * Get the target type for this <send> element.
231         *
232         * @return String Returns the targettype.
233         */
234        public final String getTargettype() {
235            return targettype;
236        }
237    
238        /**
239         * Set the target type for this <send> element.
240         *
241         * @param targettype The targettype to set.
242         */
243        public final void setTargettype(final String targettype) {
244            this.targettype = targettype;
245        }
246    
247        /**
248         * Get the event to send.
249         *
250         * @param event The event to set.
251         */
252        public final void setEvent(final String event) {
253            this.event = event;
254        }
255    
256        /**
257         * Set the event to send.
258         *
259         * @return String Returns the event.
260         */
261        public final String getEvent() {
262            return event;
263        }
264    
265        /**
266         * {@inheritDoc}
267         */
268        public void execute(final EventDispatcher evtDispatcher,
269                final ErrorReporter errRep, final SCInstance scInstance,
270                final Log appLog, final Collection derivedEvents)
271        throws ModelException, SCXMLExpressionException {
272            // Send attributes evaluation
273            TransitionTarget parentTarget = getParentTransitionTarget();
274            Context ctx = scInstance.getContext(parentTarget);
275            ctx.setLocal(getNamespacesKey(), getNamespaces());
276            Evaluator eval = scInstance.getEvaluator();
277            // Most attributes of <send> are expressions so need to be
278            // evaluated before the EventDispatcher callback
279            Object hintsValue = null;
280            if (!SCXMLHelper.isStringEmpty(hints)) {
281                hintsValue = eval.eval(ctx, hints);
282            }
283            String targetValue = target;
284            if (!SCXMLHelper.isStringEmpty(target)) {
285                targetValue = (String) eval.eval(ctx, target);
286                if (SCXMLHelper.isStringEmpty(targetValue)
287                        && appLog.isWarnEnabled()) {
288                    appLog.warn("<send>: target expression \"" + target
289                        + "\" evaluated to null or empty String");
290                }
291            }
292            String targettypeValue = targettype;
293            if (!SCXMLHelper.isStringEmpty(targettype)) {
294                targettypeValue = (String) eval.eval(ctx, targettype);
295                if (SCXMLHelper.isStringEmpty(targettypeValue)
296                        && appLog.isWarnEnabled()) {
297                    appLog.warn("<send>: targettype expression \"" + targettype
298                        + "\" evaluated to null or empty String");
299                }
300            } else {
301                // must default to 'scxml' when unspecified
302                targettypeValue = TARGETTYPE_SCXML;
303            }
304            Map params = null;
305            if (!SCXMLHelper.isStringEmpty(namelist)) {
306                StringTokenizer tkn = new StringTokenizer(namelist);
307                params = new HashMap(tkn.countTokens());
308                while (tkn.hasMoreTokens()) {
309                    String varName = tkn.nextToken();
310                    Object varObj = ctx.get(varName);
311                    if (varObj == null) {
312                        //considered as a warning here
313                        errRep.onError(ErrorConstants.UNDEFINED_VARIABLE,
314                                varName + " = null", parentTarget);
315                    }
316                    params.put(varName, varObj);
317                }
318            }
319            long wait = 0L;
320            if (!SCXMLHelper.isStringEmpty(delay)) {
321                Object delayValue = eval.eval(ctx, delay);
322                if (delayValue != null) {
323                    String delayString = delayValue.toString();
324                    wait = parseDelay(delayString, appLog);
325                }
326            }
327            String eventValue = event;
328            if (!SCXMLHelper.isStringEmpty(event)) {
329                eventValue = (String) eval.eval(ctx, event);
330                if (SCXMLHelper.isStringEmpty(eventValue)
331                        && appLog.isWarnEnabled()) {
332                    appLog.warn("<send>: event expression \"" + event
333                        + "\" evaluated to null or empty String");
334                }
335            }
336            // Lets see if we should handle it ourselves
337            if (targettypeValue != null
338                  && targettypeValue.trim().equalsIgnoreCase(TARGETTYPE_SCXML)) {
339                if (SCXMLHelper.isStringEmpty(targetValue)) {
340                    // TODO: Remove both short-circuit passes in v1.0
341                    if (wait == 0L) {
342                        if (appLog.isDebugEnabled()) {
343                            appLog.debug("<send>: Enqueued event '" + eventValue
344                                + "' with no delay");
345                        }
346                        derivedEvents.add(new TriggerEvent(eventValue,
347                            TriggerEvent.SIGNAL_EVENT, params));
348                        return;
349                    }
350                } else {
351                    // We know of no other
352                    if (appLog.isWarnEnabled()) {
353                        appLog.warn("<send>: Unavailable target - "
354                            + targetValue);
355                    }
356                    derivedEvents.add(new TriggerEvent(
357                        EVENT_ERR_SEND_TARGETUNAVAILABLE,
358                        TriggerEvent.ERROR_EVENT));
359                    // short-circuit the EventDispatcher
360                    return;
361                }
362            }
363            ctx.setLocal(getNamespacesKey(), null);
364            if (appLog.isDebugEnabled()) {
365                appLog.debug("<send>: Dispatching event '" + eventValue
366                    + "' to target '" + targetValue + "' of target type '"
367                    + targettypeValue + "' with suggested delay of " + wait
368                    + "ms");
369            }
370            // Else, let the EventDispatcher take care of it
371            evtDispatcher.send(sendid, targetValue, targettypeValue, eventValue,
372                params, hintsValue, wait, externalNodes);
373        }
374    
375        /**
376         * Parse delay.
377         *
378         * @param delayString The String value of the delay, in CSS2 format
379         * @param appLog The application log
380         * @return The parsed delay in milliseconds
381         * @throws SCXMLExpressionException If the delay cannot be parsed
382         */
383        private long parseDelay(final String delayString, final Log appLog)
384        throws SCXMLExpressionException {
385    
386            long wait = 0L;
387            long multiplier = 1L;
388    
389            if (!SCXMLHelper.isStringEmpty(delayString)) {
390    
391                String trimDelay = delayString.trim();
392                String numericDelay = trimDelay;
393                if (trimDelay.endsWith(MILLIS)) {
394                    numericDelay = trimDelay.substring(0, trimDelay.length() - 2);
395                } else if (trimDelay.endsWith(SECONDS)) {
396                    multiplier = MILLIS_IN_A_SECOND;
397                    numericDelay = trimDelay.substring(0, trimDelay.length() - 1);
398                } else if (trimDelay.endsWith(MINUTES)) { // Not CSS2
399                    multiplier = MILLIS_IN_A_MINUTE;
400                    numericDelay = trimDelay.substring(0, trimDelay.length() - 1);
401                }
402    
403                try {
404                    wait = Long.parseLong(numericDelay);
405                } catch (NumberFormatException nfe) {
406                    appLog.error(nfe.getMessage(), nfe);
407                    throw new SCXMLExpressionException(nfe.getMessage(), nfe);
408                }
409                wait *= multiplier;
410    
411            }
412    
413            return wait;
414    
415        }
416    
417        /** The suffix in the delay string for milliseconds. */
418        private static final String MILLIS = "ms";
419    
420        /** The suffix in the delay string for seconds. */
421        private static final String SECONDS = "s";
422    
423        /** The suffix in the delay string for minutes. */
424        private static final String MINUTES = "m";
425    
426        /** The number of milliseconds in a second. */
427        private static final long MILLIS_IN_A_SECOND = 1000L;
428    
429        /** The number of milliseconds in a minute. */
430        private static final long MILLIS_IN_A_MINUTE = 60000L;
431    
432    }
433