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.io;
018
019import java.io.IOException;
020import java.io.StringReader;
021import java.util.ArrayList;
022import java.util.LinkedList;
023import java.util.List;
024
025import javax.xml.stream.XMLStreamException;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogConfigurationException;
029import org.apache.commons.logging.LogFactory;
030import org.apache.commons.logging.impl.LogFactoryImpl;
031import org.apache.commons.logging.impl.SimpleLog;
032import org.apache.commons.scxml2.ActionExecutionContext;
033import org.apache.commons.scxml2.SCXMLExpressionException;
034import org.apache.commons.scxml2.SCXMLTestHelper;
035import org.apache.commons.scxml2.io.SCXMLReader.Configuration;
036import org.apache.commons.scxml2.model.Action;
037import org.apache.commons.scxml2.model.CustomAction;
038import org.apache.commons.scxml2.model.Data;
039import org.apache.commons.scxml2.model.Datamodel;
040import org.apache.commons.scxml2.model.EnterableState;
041import org.apache.commons.scxml2.model.ExternalContent;
042import org.apache.commons.scxml2.model.Final;
043import org.apache.commons.scxml2.model.ModelException;
044import org.apache.commons.scxml2.model.SCXML;
045import org.apache.commons.scxml2.model.Send;
046import org.apache.commons.scxml2.model.State;
047import org.apache.commons.scxml2.model.Transition;
048import org.junit.After;
049import org.junit.AfterClass;
050import org.junit.Assert;
051import org.junit.Before;
052import org.junit.BeforeClass;
053import org.junit.Test;
054import org.w3c.dom.Node;
055
056/**
057 * Unit tests {@link org.apache.commons.scxml2.io.SCXMLReader}.
058 */
059public class SCXMLReaderTest {
060
061    private static String oldLogFactoryProperty;
062
063    private Log scxmlReaderLog;
064
065    @BeforeClass
066    public static void beforeClass() {
067        oldLogFactoryProperty = System.getProperty(LogFactory.FACTORY_PROPERTY);
068        System.setProperty(LogFactory.FACTORY_PROPERTY, RecordingLogFactory.class.getName());
069    }
070
071    @AfterClass
072    public static void afterClass() {
073        if (oldLogFactoryProperty == null) {
074            System.clearProperty(LogFactory.FACTORY_PROPERTY);
075        } else {
076            System.setProperty(LogFactory.FACTORY_PROPERTY, oldLogFactoryProperty);
077        }
078    }
079
080    /**
081     * Set up instance variables required by this test case.
082     */
083    @Before
084    public void before() {
085        scxmlReaderLog = LogFactory.getLog(SCXMLReader.class);
086        clearRecordedLogMessages();
087    }
088
089    /**
090     * Test the implementation
091     */    
092    @Test
093    public void testSCXMLReaderMicrowave03Sample() throws Exception {
094        SCXML scxml = SCXMLTestHelper.parse("org/apache/commons/scxml2/env/jexl/microwave-03.xml");
095        Assert.assertNotNull(scxml);
096        Assert.assertNotNull(serialize(scxml));
097    }
098    
099    @Test
100    public void testSCXMLReaderMicrowave04Sample() throws Exception {
101        SCXML scxml = SCXMLTestHelper.parse("org/apache/commons/scxml2/env/jexl/microwave-04.xml");
102        Assert.assertNotNull(scxml);
103        Assert.assertNotNull(serialize(scxml));
104    }
105    
106    @Test
107    public void testSCXMLReaderTransitions01Sample() throws Exception {
108        SCXML scxml = SCXMLTestHelper.parse("org/apache/commons/scxml2/transitions-01.xml");
109        Assert.assertNotNull(scxml);
110        Assert.assertNotNull(serialize(scxml));
111    }
112    
113    @Test
114    public void testSCXMLReaderPrefix01Sample() throws Exception {
115        SCXML scxml = SCXMLTestHelper.parse("org/apache/commons/scxml2/prefix-01.xml");
116        Assert.assertNotNull(scxml);
117        Assert.assertNotNull(serialize(scxml));
118    }
119    
120    @Test
121    public void testSCXMLReaderSend01Sample() throws Exception {
122        SCXML scxml = SCXMLTestHelper.parse("org/apache/commons/scxml2/send-01.xml");
123        State ten = (State) scxml.getInitialTransition().getTargets().iterator().next();
124        Assert.assertEquals("ten", ten.getId());
125        List<Transition> ten_done = ten.getTransitionsList("done.state.ten");
126        Assert.assertEquals(1, ten_done.size());
127        Transition ten2twenty = ten_done.get(0);
128        List<Action> actions = ten2twenty.getActions();
129        Assert.assertEquals(1, actions.size());
130        Send send = (Send) actions.get(0);
131        Assert.assertEquals("send1", send.getId());
132        /* Serialize
133        scxmlAsString = serialize(scxml);
134        Assert.assertNotNull(scxmlAsString);
135        String expectedFoo2Serialization =
136            "<foo xmlns=\"http://my.test.namespace\" id=\"foo2\">"
137            + "<prompt xmlns=\"http://foo.bar.com/vxml3\">This is just"
138            + " an example.</prompt></foo>";
139        Assert.assertFalse(scxmlAsString.indexOf(expectedFoo2Serialization) == -1);
140        */
141    }
142    
143    @Test
144    public void testSCXMLReaderInitialAttr() throws Exception {
145        SCXML scxml = SCXMLTestHelper.parse("org/apache/commons/scxml2/io/scxml-initial-attr.xml");
146        Assert.assertNotNull(scxml);
147        Assert.assertNotNull(serialize(scxml));
148        Final foo = (Final) scxml.getInitialTransition().getTargets().iterator().next();
149        Assert.assertEquals("foo", foo.getId());
150    }
151
152    @Test
153    public void testSCXMLValidTransitionTargets() throws Exception {
154        // ModelUpdater will fail on invalid transition targets
155        SCXMLTestHelper.parse(SCXMLTestHelper.getResource("org/apache/commons/scxml2/io/scxml-valid-transition-targets-test.xml"));
156    }
157
158    @Test(expected=org.apache.commons.scxml2.model.ModelException.class)
159    public void testSCXMLInValidTransitionTargets1() throws Exception {
160        // ModelUpdater will fail on invalid transition targets
161        SCXMLTestHelper.parse(SCXMLTestHelper.getResource("org/apache/commons/scxml2/io/scxml-invalid-transition-targets-test1.xml"));
162    }
163
164    @Test(expected=org.apache.commons.scxml2.model.ModelException.class)
165    public void testSCXMLInValidTransitionTargets2() throws Exception {
166        // ModelUpdater will fail on invalid transition targets
167        SCXMLTestHelper.parse(SCXMLTestHelper.getResource("org/apache/commons/scxml2/io/scxml-invalid-transition-targets-test2.xml"));
168    }
169
170    @Test
171    public void testSCXMLReaderCustomActionWithBodyTextSample() throws Exception {
172        List<CustomAction> cas = new ArrayList<CustomAction>();
173        CustomAction ca = new CustomAction("http://my.custom-actions.domain",
174            "action", MyAction.class);
175        cas.add(ca);
176        SCXML scxml = SCXMLTestHelper.parse("org/apache/commons/scxml2/io/custom-action-body-test-1.xml", cas);
177        EnterableState state = (EnterableState) scxml.getInitialTransition().getTargets().iterator().next();
178        Assert.assertEquals("actions", state.getId());
179        List<Action> actions = state.getOnEntries().get(0).getActions();
180        Assert.assertEquals(1, actions.size());
181        MyAction my = (MyAction) actions.get(0);
182        Assert.assertNotNull(my);
183        Assert.assertTrue(my.getExternalNodes().size() > 0);
184    }
185
186    @Test
187    public void testSCXMLReaderWithInvalidElements() throws Exception {
188        // In the default lenient/verbose mode (strict == false && silent == false),
189        // the model exception should be just logged without a model exception.
190        Configuration configuration = new Configuration();
191        SCXML scxml = SCXMLReader.read(SCXMLTestHelper.getResource("org/apache/commons/scxml2/io/scxml-with-invalid-elems.xml"),
192                        configuration);
193        Assert.assertNotNull(scxml);
194        Assert.assertNotNull(serialize(scxml));
195        Final foo = (Final) scxml.getInitialTransition().getTargets().iterator().next();
196        Assert.assertEquals("foo", foo.getId());
197        Datamodel dataModel = scxml.getDatamodel();
198        Assert.assertNotNull(dataModel);
199        List<Data> dataList = dataModel.getData();
200        Assert.assertEquals(1, dataList.size());
201        Assert.assertEquals("time", dataList.get(0).getId());
202        assertContainsRecordedLogMessage("Ignoring unknown or invalid element <baddata> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <datamodel>");
203        assertContainsRecordedLogMessage("Ignoring unknown or invalid element <baddata> in namespace \"http://www.example.com/scxml\" as child of <datamodel>");
204        assertContainsRecordedLogMessage("Ignoring unknown or invalid element <trace> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <onentry>");
205        assertContainsRecordedLogMessage("Ignoring unknown or invalid element <onbeforeexit> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <final>");
206
207        // In the lenient/silent mode (strict == false && silent == true),
208        // no model exception is logged.
209        clearRecordedLogMessages();
210        scxml = null;
211        configuration = new Configuration();
212        configuration.setStrict(false);
213        configuration.setSilent(true);
214        scxml = SCXMLReader.read(SCXMLTestHelper.getResource("org/apache/commons/scxml2/io/scxml-with-invalid-elems.xml"),
215                configuration);
216        Assert.assertNotNull(scxml);
217        Assert.assertNotNull(serialize(scxml));
218        foo = (Final) scxml.getInitialTransition().getTargets().iterator().next();
219        Assert.assertEquals("foo", foo.getId());
220        dataModel = scxml.getDatamodel();
221        Assert.assertNotNull(dataModel);
222        dataList = dataModel.getData();
223        Assert.assertEquals(1, dataList.size());
224        Assert.assertEquals("time", dataList.get(0).getId());
225        assertNotContainsRecordedLogMessage("Ignoring unknown or invalid element <baddata> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <datamodel>");
226        assertNotContainsRecordedLogMessage("Ignoring unknown or invalid element <baddata> in namespace \"http://www.example.com/scxml\" as child of <datamodel>");
227        assertNotContainsRecordedLogMessage("Ignoring unknown or invalid element <trace> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <onentry>");
228        assertNotContainsRecordedLogMessage("Ignoring unknown or invalid element <onbeforeexit> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <final>");
229
230        // In strict/verbose mode (strict == true && silent == false), it should fail to read the model and catch a model exception
231        // with warning logs because of the invalid <baddata> element.
232        clearRecordedLogMessages();
233        scxml = null;
234        configuration = new Configuration();
235        configuration.setStrict(true);
236        configuration.setSilent(false);
237        try {
238            scxml = SCXMLReader.read(SCXMLTestHelper.getResource("org/apache/commons/scxml2/io/scxml-with-invalid-elems.xml"),
239                    configuration);
240            Assert.fail("In strict mode, it should have thrown a model exception.");
241        } catch (ModelException e) {
242            Assert.assertTrue(e.getMessage().contains("Ignoring unknown or invalid element <baddata>"));
243        }
244        assertContainsRecordedLogMessage("Ignoring unknown or invalid element <baddata> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <datamodel>");
245        assertContainsRecordedLogMessage("Ignoring unknown or invalid element <baddata> in namespace \"http://www.example.com/scxml\" as child of <datamodel>");
246        assertContainsRecordedLogMessage("Ignoring unknown or invalid element <trace> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <onentry>");
247        assertContainsRecordedLogMessage("Ignoring unknown or invalid element <onbeforeexit> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <final>");
248
249        // In strict/silent mode (strict == true && silent == true), it should fail to read the model and catch a model exception
250        // without warning logs because of the invalid <baddata> element.
251        clearRecordedLogMessages();
252        scxml = null;
253        configuration = new Configuration();
254        configuration.setStrict(true);
255        configuration.setSilent(true);
256        try {
257            scxml = SCXMLReader.read(SCXMLTestHelper.getResource("org/apache/commons/scxml2/io/scxml-with-invalid-elems.xml"),
258                    configuration);
259            Assert.fail("In strict mode, it should have thrown a model exception.");
260        } catch (ModelException e) {
261            Assert.assertTrue(e.getMessage().contains("Ignoring unknown or invalid element <baddata>"));
262        }
263        assertNotContainsRecordedLogMessage("Ignoring unknown or invalid element <baddata> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <datamodel>");
264        assertNotContainsRecordedLogMessage("Ignoring unknown or invalid element <baddata> in namespace \"http://www.example.com/scxml\" as child of <datamodel>");
265        assertNotContainsRecordedLogMessage("Ignoring unknown or invalid element <trace> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <onentry>");
266        assertNotContainsRecordedLogMessage("Ignoring unknown or invalid element <onbeforeexit> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <final>");
267    }
268
269    @Test
270    public void testSCXMLReaderGroovyClosure() throws Exception {
271        SCXML scxml = SCXMLTestHelper.parse("org/apache/commons/scxml2/env/groovy/groovy-closure.xml");
272        Assert.assertNotNull(scxml);
273        Assert.assertNotNull(scxml.getGlobalScript());
274        String scxmlAsString = serialize(scxml);
275        Assert.assertNotNull(scxmlAsString);
276        scxml = SCXMLTestHelper.parse(new StringReader(scxmlAsString), null);
277        Assert.assertNotNull(scxml);
278        Assert.assertNotNull(scxml.getGlobalScript());
279    }
280
281    private String serialize(final SCXML scxml) throws IOException, XMLStreamException {
282        String scxmlAsString = SCXMLWriter.write(scxml);
283        Assert.assertNotNull(scxmlAsString);
284        return scxmlAsString;
285    }
286
287    private void assertContainsRecordedLogMessage(final String message) {
288        if (scxmlReaderLog instanceof RecordingSimpleLog) {
289            Assert.assertTrue(((RecordingSimpleLog) scxmlReaderLog).containsMessage(
290                    "Ignoring unknown or invalid element <baddata> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <datamodel>"));
291        }
292    }
293
294    private void assertNotContainsRecordedLogMessage(final String message) {
295        if (scxmlReaderLog instanceof RecordingSimpleLog) {
296            Assert.assertFalse(((RecordingSimpleLog) scxmlReaderLog).containsMessage(
297                    "Ignoring unknown or invalid element <baddata> in namespace \"http://www.w3.org/2005/07/scxml\" as child of <datamodel>"));
298        }
299    }
300
301    private void clearRecordedLogMessages() {
302        if (scxmlReaderLog instanceof RecordingSimpleLog) {
303            ((RecordingSimpleLog) scxmlReaderLog).clearMessages();
304        }
305    }
306
307    public static class MyAction extends Action implements ExternalContent {
308        private static final long serialVersionUID = 1L;
309
310        private List<Node> nodes = new ArrayList<Node>();
311
312        @Override
313        public void execute(ActionExecutionContext exctx) throws ModelException, SCXMLExpressionException {
314            // Not relevant to test
315        }
316
317        @Override
318        public List<Node> getExternalNodes() {
319            return nodes;
320        }
321
322    }
323
324    /**
325     * Custom LogFactory implementation to capture log messages for logging verification.
326     */
327    public static class RecordingLogFactory extends LogFactoryImpl {
328        @Override
329        protected Log newInstance(String name) throws LogConfigurationException {
330            return new RecordingSimpleLog(name);
331        }
332    }
333
334    /**
335     * Custom Simple Log implemenation capturing log messages
336     */
337    public static class RecordingSimpleLog extends SimpleLog {
338
339        private static final long serialVersionUID = 1L;
340
341        private List<String> messages = new LinkedList<String>();
342
343        public RecordingSimpleLog(String name) {
344            super(name);
345        }
346
347        /**
348         * Clear all the recorded log messages.
349         */
350        public void clearMessages() {
351            messages.clear();
352        }
353
354        /**
355         * Return true if msg is found in any recorded log messages.
356         * @param msg
357         * @return
358         */
359        public boolean containsMessage(final String msg) {
360            for (String message : messages) {
361                if (message.contains(msg)) {
362                    return true;
363                }
364            }
365            return false;
366        }
367
368        @Override
369        protected boolean isLevelEnabled(int logLevel) {
370            return (logLevel >= LOG_LEVEL_INFO);
371        }
372
373        @Override
374        protected void log(int type, Object message, Throwable t) {
375            super.log(type, message, t);
376            messages.add(message.toString());
377        }
378    }
379}
380