diff --git a/docs/src/reference/asciidoc/images/papyrus-gs-22.png b/docs/src/reference/asciidoc/images/papyrus-gs-22.png new file mode 100644 index 00000000..3e0c4627 Binary files /dev/null and b/docs/src/reference/asciidoc/images/papyrus-gs-22.png differ diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index c2284cb1..74bd7bb7 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -2723,6 +2723,27 @@ sub-state. image::images/papyrus-gs-15.png[scaledwidth="100%"] +[[sm-papyrus-import]] +=== Using a Machine Import +It's also possible to use import functionality where uml files can reference to other models. + +image::images/papyrus-gs-22.png[scaledwidth="100%"] + +Within `UmlStateMachineModelFactory` it's possible to use additional resources or locations +to define referenced model files. + +==== +[source,java,indent=0] +---- +include::samples/DocsUmlSampleTests1.java[tags=snippetC] +---- +==== + +IMPORTANT: Links between files in uml models needs to be relative as +otherwise things break when model files are copied out from a +classpath to a temporary directory so that eclipse parsing classes can +read those. + [[sm-repository]] == Repository Support diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/ResourcerResolver.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/ResourcerResolver.java new file mode 100644 index 00000000..9f8192b7 --- /dev/null +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/ResourcerResolver.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.uml; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.stream.Stream; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.statemachine.StateMachineException; +import org.springframework.util.FileCopyUtils; + +/** + * Support class to resolve uml resources and handling a cases where things + * needs to be copied away from a classpath trying to handle common relative + * paths with a locations or resource paths in case of a cross relative linking. + * + * @author Janne Valkealahti + */ +class ResourcerResolver { + + private ResourceLoader resourceLoader; + private Resource mainResource; + private String mainLocation; + private Resource[] additionalResources; + private String[] additionalLocations; + + public ResourcerResolver(Resource mainResource, Resource[] additionalResources) { + this.mainResource = mainResource; + this.additionalResources = additionalResources != null ? additionalResources : new Resource[0]; + } + + public ResourcerResolver(ResourceLoader resourceLoader, String mainLocation, String[] additionalLocations) { + this.resourceLoader = resourceLoader; + this.mainLocation = mainLocation; + this.additionalLocations = additionalLocations != null ? additionalLocations : new String[0]; + } + + public Holder[] resolve() { + ArrayList holders = new ArrayList<>(); + if (mainLocation != null) { + Resource[] resources = Stream.concat(Stream.of(mainLocation), Stream.of(additionalLocations)) + .map(location -> resourceLoader.getResource(location)) + .toArray(Resource[]::new); + return getResourceUris(resources); + } else if (mainResource != null) { + Resource[] resources = Stream.concat(Stream.of(mainResource), Stream.of(additionalResources)) + .toArray(Resource[]::new); + return getResourceUris(resources); + } + return holders.toArray(new Holder[0]); + } + + private Holder[] getResourceUris(Resource... resources) { + ArrayList holders = new ArrayList<>(); + try { + if (allPhysical(resources)) { + for (Resource resource : resources) { + holders.add(new Holder(resource.getFile().toURI())); + } + } else { + Path tempDir = Files.createTempDirectory(null); + for (Resource resource : resources) { + if (resource instanceof ClassPathResource) { + ClassPathResource cpr = (ClassPathResource)resource; + File f = new File(tempDir.toFile(), cpr.getPath()); + f.getParentFile().mkdirs(); + FileCopyUtils.copy(resource.getInputStream(), new FileOutputStream(f)); + holders.add(new Holder(f.toURI(), f.toPath())); + } + } + } + } catch (IOException e) { + throw new StateMachineException(e); + } + return holders.toArray(new Holder[0]); + } + + private boolean isPhysical(Resource resource) { + try { + resource.getFile(); + return true; + } catch (Exception e) { + } + return false; + } + + private boolean allPhysical(Resource... resources) { + for (Resource resource : resources) { + if (!isPhysical(resource)) { + return false; + } + } + return true; + } + + static class Holder { + URI uri; + Path path; + + public Holder(URI uri) { + this(uri, null); + } + + public Holder(URI uri, Path path) { + this.uri = uri; + this.path = path; + } + + public URI getUri() { + return uri; + } + + public Path getPath() { + return path; + } + } +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/UmlStateMachineModelFactory.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/UmlStateMachineModelFactory.java index 94501396..941fb1d2 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/UmlStateMachineModelFactory.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/UmlStateMachineModelFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 the original author or authors. + * Copyright 2016-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,7 @@ */ package org.springframework.statemachine.uml; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.URI; import java.nio.file.Files; -import java.nio.file.Path; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.uml2.uml.Model; @@ -29,23 +25,33 @@ import org.springframework.statemachine.config.model.AbstractStateMachineModelFa import org.springframework.statemachine.config.model.DefaultStateMachineModel; import org.springframework.statemachine.config.model.StateMachineModel; import org.springframework.statemachine.config.model.StateMachineModelFactory; +import org.springframework.statemachine.uml.ResourcerResolver.Holder; import org.springframework.statemachine.uml.support.UmlModelParser; import org.springframework.statemachine.uml.support.UmlModelParser.DataHolder; import org.springframework.statemachine.uml.support.UmlUtils; import org.springframework.util.Assert; -import org.springframework.util.FileCopyUtils; /** * {@link StateMachineModelFactory} which builds {@link StateMachineModel} from * uml representation. * + * {@code resource} or {@code location} is a main uml file used as a source + * passed to parser classes. {@code additionalResources} and {@code additionalLocations} + * are needed if uml model have references or links to additional uml files as an + * import. In case of a these files being located in a classpath which is inside of + * a jar, files are copied out into filesystem as eclipse uml libs can only parse + * physical files. In a case of this a common "path" from all resources are resolved + * and copied into filesystem with a structure so that at least relative links in uml + * files will work. + * * @author Janne Valkealahti */ -public class UmlStateMachineModelFactory extends AbstractStateMachineModelFactory - implements StateMachineModelFactory { +public class UmlStateMachineModelFactory extends AbstractStateMachineModelFactory { private Resource resource; private String location; + private Resource[] additionalResources; + private String[] additionalLocations; /** * Instantiates a new uml state machine model factory. @@ -53,8 +59,7 @@ public class UmlStateMachineModelFactory extends AbstractStateMachineModelFactor * @param resource the resource */ public UmlStateMachineModelFactory(Resource resource) { - Assert.notNull(resource, "Resource must be set"); - this.resource = resource; + this(resource, null); } /** @@ -63,20 +68,51 @@ public class UmlStateMachineModelFactory extends AbstractStateMachineModelFactor * @param location the resource location */ public UmlStateMachineModelFactory(String location) { + this(location, null); + } + + /** + * Instantiates a new uml state machine model factory. + * + * @param resource the resource + * @param additionalResources the additional resources + */ + public UmlStateMachineModelFactory(Resource resource, Resource[] additionalResources) { + Assert.notNull(resource, "Resource must be set"); + this.resource = resource; + this.additionalResources = additionalResources; + } + + /** + * Instantiates a new uml state machine model factory. + * + * @param location the resource location + * @param additionalLocations the additional locations + */ + public UmlStateMachineModelFactory(String location, String[] additionalLocations) { Assert.notNull(location, "Location must be set"); this.location = location; + this.additionalLocations = additionalLocations; } @Override public StateMachineModel build() { - Model model = null; + ResourcerResolver resourceResolver = null; + if (this.location != null) { + resourceResolver = new ResourcerResolver(getResourceLoader(), location, additionalLocations); + } else if (this.resource != null) { + resourceResolver = new ResourcerResolver(resource, additionalResources); + } + Holder holder = null; + Model model = null; org.eclipse.emf.ecore.resource.Resource resource = null; try { - holder = getResourceUri(resolveResource()); + Holder[] resources = resourceResolver.resolve(); + holder = resources != null && resources.length > 0 ? resources[0] : null; resource = UmlUtils.getResource(holder.uri.getPath()); model = (Model) EcoreUtil.getObjectByType(resource.getContents(), UMLPackage.Literals.MODEL); - } catch (IOException e) { + } catch (Exception e) { throw new IllegalArgumentException("Cannot build build model from resource " + resource + " or location " + location, e); } finally { // if we have a path, tmp file were created, clean it @@ -87,6 +123,7 @@ public class UmlStateMachineModelFactory extends AbstractStateMachineModelFactor } } } + UmlModelParser parser = new UmlModelParser(model, this); DataHolder dataHolder = parser.parseModel(); @@ -107,39 +144,4 @@ public class UmlStateMachineModelFactory extends AbstractStateMachineModelFactor // we don't set configurationData here, so assume null return new DefaultStateMachineModel(null, dataHolder.getStatesData(), dataHolder.getTransitionsData()); } - - private Resource resolveResource() { - if (resource != null) { - return resource; - } else { - return getResourceLoader().getResource(location); - } - } - - private Holder getResourceUri(Resource resource) throws IOException { - // try to see if resource is an actual File and eclipse - // libs cannot use input stream. thus creating a tmp file with - // needed .uml prefix and getting URI from there. - try { - return new Holder(resource.getFile().toURI()); - } catch (Exception e) { - } - Path tempFile = Files.createTempFile(null, ".uml"); - FileCopyUtils.copy(resource.getInputStream(), new FileOutputStream(tempFile.toFile())); - return new Holder(tempFile.toUri(), tempFile); - } - - private static class Holder { - URI uri; - Path path; - - public Holder(URI uri) { - this(uri, null); - } - - public Holder(URI uri, Path path) { - this.uri = uri; - this.path = path; - } - } } diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java index 0647690f..bec2a46c 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,8 +69,9 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * Model parser which constructs states and transitions data out from - * an uml model. + * Model parser which constructs states and transitions data out from an uml + * model. This implementation is not thread safe and model parsing can only be + * used once per instance. * * @author Janne Valkealahti */ @@ -92,6 +93,10 @@ public class UmlModelParser { private final AtomicInteger pseudostateNamingCounter = new AtomicInteger(1); private final Map pseudostateNaming = new HashMap<>(); + private final List seenStateData = new ArrayList<>(); + private final List seenEntryData = new ArrayList<>(); + private final List seenExitData = new ArrayList<>(); + private final List seenTransitionData = new ArrayList<>(); /** * Instantiates a new uml model parser. @@ -158,6 +163,46 @@ public class UmlModelParser { }); } + private void addStateData(StateData stateData) { + String key = stateData.getState(); + if (!seenStateData.contains(key)) { + stateDatas.add(stateData); + seenStateData.add(key); + } + } + + private void addEntryData(EntryData entryData) { + String skey = entryData.getSource() != null ? entryData.getSource() : "null"; + String tkey = entryData.getTarget() != null ? entryData.getTarget() : "null"; + String key = skey + "_" + tkey; + if (!seenEntryData.contains(key)) { + entrys.add(entryData); + seenEntryData.add(key); + } + } + + private void addExitData(ExitData exitData) { + String skey = exitData.getSource() != null ? exitData.getSource() : "null"; + String tkey = exitData.getTarget() != null ? exitData.getTarget() : "null"; + String key = skey + "_" + tkey; + if (!seenExitData.contains(key)) { + exits.add(exitData); + seenExitData.add(key); + } + } + + private void addTransitionData(TransitionData transitionData) { + String skey = transitionData.getSource() != null ? transitionData.getSource() : "null"; + String tkey = transitionData.getTarget() != null ? transitionData.getTarget() : "null"; + String ekey = transitionData.getEvent() != null ? transitionData.getEvent() : "null"; + String kkey = transitionData.getKind() != null ? transitionData.getKind().toString() : "null"; + String key = skey + "_" + tkey + "_" + ekey + "_" + kkey; + if (!seenTransitionData.contains(key)) { + transitionDatas.add(transitionData); + seenTransitionData.add(key); + } + } + private void handleRegion(Region region) { // build states for (Vertex vertex : region.getSubvertices()) { @@ -192,7 +237,7 @@ public class UmlModelParser { if (UmlUtils.isFinalState(state)) { stateData.setEnd(true); } - stateDatas.add(stateData); + addStateData(stateData); // add states via entry/exit reference points for (ConnectionPointReference cpr : state.getConnections()) { @@ -200,14 +245,14 @@ public class UmlModelParser { for (Pseudostate cp : cpr.getEntries()) { StateData cpStateData = new StateData<>(parent, regionId, cp.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.ENTRY); - stateDatas.add(cpStateData); + addStateData(cpStateData); } } if (cpr.getExits() != null) { for (Pseudostate cp : cpr.getExits()) { StateData cpStateData = new StateData<>(parent, regionId, cp.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.EXIT); - stateDatas.add(cpStateData); + addStateData(cpStateData); } } } @@ -223,13 +268,18 @@ public class UmlModelParser { if (kind != null) { StateData cpStateData = new StateData<>(parent, regionId, cp.getName(), false); cpStateData.setPseudoStateKind(kind); - stateDatas.add(cpStateData); + addStateData(cpStateData); } } - // do recursive handling of regions - for (Region sub : state.getRegions()) { - handleRegion(sub); + if (!state.getRegions().isEmpty()) { + // do recursive handling of regions + for (Region sub : state.getRegions()) { + handleRegion(sub); + } + } else if (state.getSubmachine() != null) { + // submachine would be there i.e. with import + handleStateMachine(state.getSubmachine()); } } // pseudostates like choice, etc @@ -256,27 +306,27 @@ public class UmlModelParser { if (state.getKind() == PseudostateKind.CHOICE_LITERAL) { StateData cpStateData = new StateData<>(parent, regionId, resolveName(state), false); cpStateData.setPseudoStateKind(PseudoStateKind.CHOICE); - stateDatas.add(cpStateData); + addStateData(cpStateData); } else if (state.getKind() == PseudostateKind.JUNCTION_LITERAL) { StateData cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.JUNCTION); - stateDatas.add(cpStateData); + addStateData(cpStateData); } else if (state.getKind() == PseudostateKind.FORK_LITERAL) { StateData cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.FORK); - stateDatas.add(cpStateData); + addStateData(cpStateData); } else if (state.getKind() == PseudostateKind.JOIN_LITERAL) { StateData cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.JOIN); - stateDatas.add(cpStateData); + addStateData(cpStateData); } else if (state.getKind() == PseudostateKind.SHALLOW_HISTORY_LITERAL) { StateData cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.HISTORY_SHALLOW); - stateDatas.add(cpStateData); + addStateData(cpStateData); } else if (state.getKind() == PseudostateKind.DEEP_HISTORY_LITERAL) { StateData cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.HISTORY_DEEP); - stateDatas.add(cpStateData); + addStateData(cpStateData); } } } @@ -296,19 +346,19 @@ public class UmlModelParser { // realistic with state machines EList cprentries = ((ConnectionPointReference)transition.getSource()).getEntries(); if (cprentries != null && cprentries.size() == 1 && cprentries.get(0).getKind() == PseudostateKind.ENTRY_POINT_LITERAL) { - entrys.add(new EntryData(cprentries.get(0).getName(), resolveName(transition.getTarget()))); + addEntryData(new EntryData(cprentries.get(0).getName(), resolveName(transition.getTarget()))); } EList cprexits = ((ConnectionPointReference)transition.getSource()).getExits(); if (cprexits != null && cprexits.size() == 1 && cprexits.get(0).getKind() == PseudostateKind.EXIT_POINT_LITERAL) { - exits.add(new ExitData(cprexits.get(0).getName(), resolveName(transition.getTarget()))); + addExitData(new ExitData(cprexits.get(0).getName(), resolveName(transition.getTarget()))); } } if (transition.getSource() instanceof Pseudostate) { if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.ENTRY_POINT_LITERAL) { - entrys.add(new EntryData(resolveName(transition.getSource()), resolveName(transition.getTarget()))); + addEntryData(new EntryData(resolveName(transition.getSource()), resolveName(transition.getTarget()))); } else if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.EXIT_POINT_LITERAL) { - exits.add(new ExitData(resolveName(transition.getSource()), resolveName(transition.getTarget()))); + addExitData(new ExitData(resolveName(transition.getSource()), resolveName(transition.getTarget()))); } else if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.CHOICE_LITERAL) { LinkedList> list = choices.get(resolveName(transition.getSource())); if (list == null) { @@ -373,12 +423,12 @@ public class UmlModelParser { if (transition.getTarget() instanceof ConnectionPointReference) { EList cprentries = ((ConnectionPointReference)transition.getTarget()).getEntries(); if (cprentries != null && cprentries.size() == 1) { - transitionDatas.add(new TransitionData(resolveName(transition.getSource()), + addTransitionData(new TransitionData(resolveName(transition.getSource()), cprentries.get(0).getName(), signal.getName(), UmlUtils.resolveTransitionActions(transition, resolver), guard, UmlUtils.mapUmlTransitionType(transition))); } } else { - transitionDatas.add(new TransitionData(resolveName(transition.getSource()), + addTransitionData(new TransitionData(resolveName(transition.getSource()), resolveName(transition.getTarget()), signal.getName(), UmlUtils.resolveTransitionActions(transition, resolver), guard, UmlUtils.mapUmlTransitionType(transition))); } @@ -391,7 +441,7 @@ public class UmlModelParser { if (timeEvent.isRelative()) { count = 1; } - transitionDatas.add(new TransitionData(resolveName(transition.getSource()), + addTransitionData(new TransitionData(resolveName(transition.getSource()), resolveName(transition.getTarget()), period, count, UmlUtils.resolveTransitionActions(transition, resolver), guard, UmlUtils.mapUmlTransitionType(transition))); } @@ -400,7 +450,7 @@ public class UmlModelParser { // create anonymous transition if needed if (shouldCreateAnonymousTransition(transition)) { - transitionDatas.add(new TransitionData(resolveName(transition.getSource()), resolveName(transition.getTarget()), + addTransitionData(new TransitionData(resolveName(transition.getSource()), resolveName(transition.getTarget()), null, UmlUtils.resolveTransitionActions(transition, resolver), resolveGuard(transition), UmlUtils.mapUmlTransitionType(transition))); } diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/ResourcerResolverTests.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/ResourcerResolverTests.java new file mode 100644 index 00000000..637f277c --- /dev/null +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/ResourcerResolverTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.uml; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.io.File; +import java.io.IOException; + +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.statemachine.uml.ResourcerResolver.Holder; + +public class ResourcerResolverTests { + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + // private String[] EMPTY_LOCATIONS = new String[0]; + + // in a below tests, + // we expect resources to resolve as a physical files + + @Test + public void testOneMainLocation() throws Exception { + String mainLocation = "classpath:org/springframework/statemachine/uml/import-main/import-main.uml"; + ResourcerResolver resolver = new ResourcerResolver(resourceLoader, mainLocation, null); + Holder[] resolved = resolver.resolve(); + assertThat(resolved, notNullValue()); + assertThat(resolved.length, is(1)); + assertThat(resolved[0].getUri().getPath(), notNullValue()); + } + + @Test + public void testOneMainResource() throws Exception { + Resource mainResource = new ClassPathResource("org/springframework/statemachine/uml/import-main/import-main.uml"); + ResourcerResolver resolver = new ResourcerResolver(mainResource, null); + Holder[] resolved = resolver.resolve(); + assertThat(resolved, notNullValue()); + assertThat(resolved.length, is(1)); + assertThat(resolved[0].getUri().getPath(), notNullValue()); + } + + @Test + public void testMultipleLocations() throws Exception { + String mainLocation = "classpath:org/springframework/statemachine/uml/import-main/import-main.uml"; + String subLocation = "classpath:org/springframework/statemachine/uml/import-sub/import-sub.uml"; + ResourcerResolver resolver = new ResourcerResolver(resourceLoader, mainLocation, new String[]{subLocation}); + Holder[] resolved = resolver.resolve(); + assertThat(resolved, notNullValue()); + assertThat(resolved.length, is(2)); + assertThat(resolved[0].getUri().getPath(), notNullValue()); + assertThat(resolved[1].getUri().getPath(), notNullValue()); + } + + @Test + public void testMultipleResources() throws Exception { + Resource mainResource = new ClassPathResource("org/springframework/statemachine/uml/import-main/import-main.uml"); + Resource subResource = new ClassPathResource("org/springframework/statemachine/uml/import-sub/import-sub.uml"); + ResourcerResolver resolver = new ResourcerResolver(mainResource, new Resource[]{subResource}); + Holder[] resolved = resolver.resolve(); + assertThat(resolved, notNullValue()); + assertThat(resolved.length, is(2)); + assertThat(resolved[0].getUri().getPath(), notNullValue()); + assertThat(resolved[1].getUri().getPath(), notNullValue()); + } + + // in a below tests, + // do monkey thing and setup resource which will not resolve to + // an actual file as would happen with file inside boot fat-jar classpath + + @Test + public void testOneMainResourceNotPhysicalFile() throws Exception { + Resource mainResource = new TestClassPathResource("org/springframework/statemachine/uml/import-main/import-main.uml"); + ResourcerResolver resolver = new ResourcerResolver(mainResource, null); + Holder[] resolved = resolver.resolve(); + assertThat(resolved, notNullValue()); + assertThat(resolved.length, is(1)); + assertThat(resolved[0].getUri().getPath(), notNullValue()); + assertThat(resolved[0].getPath().toFile().exists(), is(true)); + } + + @Test + public void testMultipleResourcesNotPhysicalFile() throws Exception { + Resource mainResource = new TestClassPathResource("org/springframework/statemachine/uml/import-main/import-main.uml"); + Resource subResource = new TestClassPathResource("org/springframework/statemachine/uml/import-sub/import-sub.uml"); + ResourcerResolver resolver = new ResourcerResolver(mainResource, new Resource[]{subResource}); + Holder[] resolved = resolver.resolve(); + assertThat(resolved, notNullValue()); + assertThat(resolved.length, is(2)); + assertThat(resolved[0].getUri().getPath(), notNullValue()); + assertThat(resolved[0].getPath().toFile().exists(), is(true)); + assertThat(resolved[1].getUri().getPath(), notNullValue()); + assertThat(resolved[1].getPath().toFile().exists(), is(true)); + } + + private static class TestClassPathResource extends ClassPathResource { + + public TestClassPathResource(String path) { + super(path); + } + + @Override + public File getFile() throws IOException { + throw new IOException(); + } + } +} diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java index cde38b17..c20bc524 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java @@ -1136,6 +1136,17 @@ public class UmlStateMachineModelFactoryTests extends AbstractUmlTests { assertThat(stateMachine.getExtendedState().get("key", String.class), is("value")); } + @Test + @SuppressWarnings("unchecked") + public void testImportedSubMachine() { + context.register(Config28.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + + stateMachine.start(); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("MAIN2", "CHILD2")); + } + @Configuration @EnableStateMachine public static class Config2 extends StateMachineConfigurerAdapter { @@ -1791,6 +1802,25 @@ public class UmlStateMachineModelFactoryTests extends AbstractUmlTests { } } + @Configuration + @EnableStateMachine + public static class Config28 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineModelConfigurer model) throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + Resource mainModel = new ClassPathResource("org/springframework/statemachine/uml/import-main/import-main.uml"); + Resource subModel = new ClassPathResource("org/springframework/statemachine/uml/import-sub/import-sub.uml"); + return new UmlStateMachineModelFactory(mainModel, new Resource[] { subModel }); + } + } + public static class LatchAction implements Action { CountDownLatch latch = new CountDownLatch(1); @Override diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/docs/DocsUmlSampleTests1.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/docs/DocsUmlSampleTests1.java index 408659ad..6b24a756 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/docs/DocsUmlSampleTests1.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/docs/DocsUmlSampleTests1.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,4 +97,25 @@ public class DocsUmlSampleTests1 { } } // end::snippetB[] + +// tag::snippetC[] + @Configuration + @EnableStateMachine + public static class Config3 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineModelConfigurer model) throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + return new UmlStateMachineModelFactory( + "classpath:org/springframework/statemachine/uml/import-main/import-main.uml", + new String[] { "classpath:org/springframework/statemachine/uml/import-sub/import-sub.uml" }); + } + } +// end::snippetC[] } diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-main/import-main.di b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-main/import-main.di new file mode 100644 index 00000000..8c549eec --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-main/import-main.di @@ -0,0 +1,2 @@ + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-main/import-main.notation b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-main/import-main.notation new file mode 100644 index 00000000..ce32452c --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-main/import-main.notation @@ -0,0 +1,93 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-main/import-main.uml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-main/import-main.uml new file mode 100644 index 00000000..cfef0147 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-main/import-main.uml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-sub/import-sub.di b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-sub/import-sub.di new file mode 100644 index 00000000..8c549eec --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-sub/import-sub.di @@ -0,0 +1,2 @@ + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-sub/import-sub.notation b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-sub/import-sub.notation new file mode 100644 index 00000000..b151c18b --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-sub/import-sub.notation @@ -0,0 +1,93 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-sub/import-sub.uml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-sub/import-sub.uml new file mode 100644 index 00000000..bd8858a7 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/import-sub/import-sub.uml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + +