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 */
017package org.apache.commons.scxml2.env.groovy;
018
019import java.lang.reflect.Array;
020import java.util.Collection;
021import java.util.Map;
022
023import org.apache.commons.scxml2.Builtin;
024import org.apache.commons.scxml2.SCXMLExpressionException;
025import org.apache.commons.scxml2.XPathBuiltin;
026
027import groovy.lang.Binding;
028import groovy.lang.MissingPropertyException;
029import groovy.lang.Script;
030
031/**
032 * Groovy {@link Script} base class for SCXML, providing the standard 'builtin' functions {@link #In(String)},
033 * {@link #Data(String)} and {@link #Location(String)}, as well as JEXL like convenience functions
034 * {@link #empty(Object)} and {@link #var(String)}.
035 */
036public abstract class GroovySCXMLScript extends Script {
037
038    GroovyContext context;
039    GroovyContextBinding binding;
040
041    protected GroovySCXMLScript() {
042        super(null);
043    }
044
045    @Override
046    public void setBinding(final Binding binding) {
047        super.setBinding(binding);
048        this.binding = (GroovyContextBinding)binding;
049        this.context = this.binding.getContext();
050    }
051
052    /**
053     * Implements the In() predicate for SCXML documents ( see Builtin#isMember )
054     * @param state The State ID to compare with
055     * @return Whether this State belongs to this Set
056     */
057    public boolean In(final String state) {
058        return Builtin.isMember(context, state);
059    }
060
061    /**
062     * Implements the Data() predicate for SCXML documents.
063     * @param expression the XPath expression
064     * @return the data matching the expression
065     */
066    public Object Data(final String expression) throws SCXMLExpressionException {
067        return XPathBuiltin.eval(context, expression);
068    }
069
070    /**
071     * Implements the Location() predicate for SCXML documents.
072     * @param location the XPath expression
073     * @return the location list for the location expression
074     */
075    public Object Location(final String location) throws SCXMLExpressionException {
076        return XPathBuiltin.evalLocation(context, location);
077    }
078
079    /**
080     * The var function can be used to check if a variable is defined,
081     * <p>
082     * In the Groovy language (implementation) you cannot check for an undefined variable directly:
083     * Groovy will raise a MissingPropertyException before you get the chance.
084     * </p>
085     * <p>
086     * The var function works around this by indirectly looking up the variable, which you therefore have to specify as a String.
087     * </p>
088     * <p>
089     * So, use <code>var('name')</code>, not <code>var(name)</code>
090     * </p>
091     * <p>
092     * Note: this function doesn't support object navigation, like <code>var('name.property')</code>.<br/>
093     * Instead, once you established a variable 'name' exists, you <em>thereafter</em> can use the standard Groovy
094     * Safe Navigation operator (?.), like so: <code>name?.property</code>.<br/>
095     * See for more information: <a href="http://docs.codehaus.org/display/GROOVY/Operators#Operators-SafeNavigationOperator(?.)">Groovy SafeNavigationOperator</a>
096     * </p>
097     */
098    public boolean var(String property) {
099        if (!context.has(property)) {
100            try {
101                getMetaClass().getProperty(this, property);
102            } catch (MissingPropertyException e) {
103                return false;
104            }
105        }
106        return true;
107    }
108
109    /**
110     * The empty function mimics the behavior of the JEXL empty function, in that it returns true if the parameter is:
111     * <ul>
112     *     <li>null, or</li>
113     *     <li>an empty String, or</li>
114     *     <li>an zero length Array, or</li>
115     *     <li>an empty Collection, or</li>
116     *     <li>an empty Map</li>
117     * </ul>
118     * <p>
119     *     Note: one difference with the JEXL language is that Groovy doesn't allow checking for undefined variables.<br/>
120     *     Before being able to check, Groovy will already have raised an MissingPropertyException if the variable cannot be found.<br/>
121     *     To work around this, the custom {@link #var(String)} function is available.
122     * </p>
123     */
124    public boolean empty(Object obj) {
125        return obj == null ||
126                (obj instanceof String && ((String)obj).isEmpty()) ||
127                ((obj.getClass().isArray() && Array.getLength(obj)==0)) ||
128                (obj instanceof Collection && ((Collection)obj).size()==0) ||
129                (obj instanceof Map && ((Map)obj).isEmpty());
130    }
131}