| <html> |
| <head> |
| <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" > |
| <title>Advanced Features</title> |
| |
| <link href="book.css" rel="stylesheet" type="text/css"> |
| <link href="code.css" rel="stylesheet" type="text/css"> |
| <link rel="home" href="00-Main.html" title=""> |
| </head> |
| <body> |
| <a name="AdvancedFeatures"></a> |
| <h1>Advanced Features</h1> |
| <p> |
| In this chapter we describe some advanced features. |
| </p> |
| <a name="BuildingWithMaven"></a> |
| <h2>Building with Maven</h2> |
| <p> |
| We provide Maven artifacts in Maven Central that allows you to compile the Parsley DSL sources |
| with Maven/Tycho, using the <em>xtext-maven-plugin</em>. |
| </p> |
| <p> |
| This is a typical configuration (a complete example can be found here: |
| <a href="http://git.eclipse.org/c/emf-parsley/org.eclipse.emf-parsley.git/tree/examples/org.eclipse.emf.parsley.examples.maven">http://git.eclipse.org/c/emf-parsley/org.eclipse.emf-parsley.git/tree/examples/org.eclipse.emf.parsley.examples.maven</a>): |
| </p> |
| <p> |
| <div class="literallayout"> |
| <div class="incode"> |
| <p class="code"> |
| <plugin><br/> |
| <groupId>org.eclipse.xtext</groupId><br/> |
| <artifactId>xtext-maven-plugin</artifactId><br/> |
| <version>${xtext-version}</version><br/> |
| <executions><br/> |
| <execution><br/> |
| <goals><br/> |
| <goal>generate</goal><br/> |
| </goals><br/> |
| </execution><br/> |
| </executions><br/> |
| <configuration><br/> |
| <languages><br/> |
| <language><br/> |
| <setup>org.eclipse.emf.parsley.dsl.EmfParsleyDslStandaloneSetup</setup><br/> |
| <outputConfigurations><br/> |
| <outputConfiguration><br/> |
| <outputDirectory>${basedir}/emfparsley-gen</outputDirectory><br/> |
| </outputConfiguration><br/> |
| </outputConfigurations><br/> |
| </language><br/> |
| </languages><br/> |
| </configuration><br/> |
| <dependencies><br/> |
| <dependency><br/> |
| <groupId>org.eclipse.emf.parsley</groupId><br/> |
| <artifactId>org.eclipse.emf.parsley.dsl.standalone</artifactId><br/> |
| <version>${parsley-version}</version><br/> |
| </dependency><br/> |
| </dependencies><br/> |
| </plugin><br/> |
| </p> |
| </div> |
| </div> |
| </p> |
| <a name="Testing"></a> |
| <h2>Testing Framework</h2> |
| <p> |
| We provide some utility classes for testing <em>EMF Parsley</em> components in the feature |
| "EMF Parsley Junit4 Support". By deriving from one of the abstract classes in our |
| testing bundle, you will be able to write tests that are meant to be run as Junit test, |
| that is to say, NOT as Plug-in Junit tests. Thus, you will not need a running Eclipse product |
| to execute such tests: they will be much faster. Indeed, many parts of Parsley can |
| be tested even without a running Eclipse. |
| </p> |
| <p> |
| <ul> |
| <li> |
| <a class="jdoc" href="http://download.eclipse.org/modeling/emf/emf/javadoc/2.10.0/org/eclipse/emf/parsley/junit4/AbstractEmfParsleyTest.html" title="View JavaDoc"><abbr title="org.eclipse.emf.parsley.junit4.AbstractEmfParsleyTest" >AbstractEmfParsleyTest</abbr></a> <a class="srcLink" href="https://github.com/eclipse/emf/blob/R2_9_0/plugins/org.eclipse.emf.ecore/src/org/eclipse/emf/parsley/junit4/AbstractEmfParsleyTest.java" title="View Source Code" >(src)</a>: this provides |
| a few utility methods, e.g., for creating an <a class="jdoc" href="https://google.github.io/guice/api-docs/latest/javadoc/com/google/inject/Injector.html" title="View JavaDoc"><abbr title="com.google.inject.Injector" >Injector</abbr></a>. |
| </li> |
| <li> |
| <a class="jdoc" href="http://download.eclipse.org/modeling/emf/emf/javadoc/2.10.0/org/eclipse/emf/parsley/junit4/AbstractEmfParsleyShellBasedTest.html" title="View JavaDoc"><abbr title="org.eclipse.emf.parsley.junit4.AbstractEmfParsleyShellBasedTest" >AbstractEmfParsleyShellBasedTest</abbr></a> <a class="srcLink" href="https://github.com/eclipse/emf/blob/R2_9_0/plugins/org.eclipse.emf.ecore/src/org/eclipse/emf/parsley/junit4/AbstractEmfParsleyShellBasedTest.java" title="View Source Code" >(src)</a>: this allows |
| to run Junit tests that require a <a class="jdoc" href="http://help.eclipse.org/helios/topic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/widgets/Display.html" title="View JavaDoc"><abbr title="org.eclipse.swt.widgets.Display" >Display</abbr></a> and a |
| <a class="jdoc" href="http://help.eclipse.org/helios/topic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/widgets/Shell.html" title="View JavaDoc"><abbr title="org.eclipse.swt.widgets.Shell" >Shell</abbr></a>. |
| </li> |
| <li> |
| <a class="jdoc" href="http://download.eclipse.org/modeling/emf/emf/javadoc/2.10.0/org/eclipse/emf/parsley/junit4/AbstractEmfParsleyControlBasedTest.html" title="View JavaDoc"><abbr title="org.eclipse.emf.parsley.junit4.AbstractEmfParsleyControlBasedTest" >AbstractEmfParsleyControlBasedTest</abbr></a> <a class="srcLink" href="https://github.com/eclipse/emf/blob/R2_9_0/plugins/org.eclipse.emf.ecore/src/org/eclipse/emf/parsley/junit4/AbstractEmfParsleyControlBasedTest.java" title="View Source Code" >(src)</a>: an extension |
| of the previous class for tests that also require databinding capabilities, e.g., |
| tests for <a class="jdoc" href="http://help.eclipse.org/helios/topic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/widgets/Control.html" title="View JavaDoc"><abbr title="org.eclipse.swt.widgets.Control" >Control</abbr></a> elements; this provides many assert |
| methods for several kinds of controls, such as <em>assertCheckbox</em>, <em>assertCombo</em>, etc. |
| </li> |
| </ul> |
| </p> |
| <p> |
| We use these classes for testing most of our classes; you might want to have a look |
| at the project <em>org.eclipse.emf.parsley.tests</em> for some usage examples. |
| </p> |
| <a name="Eclipse4"></a> |
| <h2>Eclipse 4.x</h2> |
| <p> |
| Instead of using the Extension Point mechanism, EMF Parsley leverages from DSL and Google Guice Injection. |
| </p> |
| <p> |
| Because of this, it is very easy to use it with Eclipse 4.x (e4). |
| </p> |
| <p> |
| <ol> |
| </ol> |
| </p> |
| <p> |
| <em>First Example Setup</em> |
| </p> |
| <p> |
| If you followed the steps described in section <a href="02-FirstExample.html#FirstExample" title="Go to "First Example"">First Example</a> you will have already |
| what we need to begin. Otherwise the following wizard will bring you to that point. |
| </p> |
| <p> |
| <ol> |
| <li> |
| File -> New... -> Example... |
| </li> |
| <li> |
| from Category "EMF Parsley Examples", select "EMF Parsley First Example" |
| </li> |
| <li> |
| press Next and Finish |
| </li> |
| </ol> |
| </p> |
| <p> |
| You will end up with three plug-ins: |
| </p> |
| <p> |
| <ul> |
| <li> |
| org.eclipse.emf.parsley.examples.firstexample (the EMF Parsley example plug-in) |
| </li> |
| <li> |
| org.eclipse.emf.examples.library (the model plug-in) |
| </li> |
| <li> |
| org.eclipse.emf.examples.library.edit (the model.edit plug-in) |
| </li> |
| </ul> |
| </p> |
| <p> |
| As a reminder, in section <a href="02-FirstExample.html#FirstExample" title="Go to "First Example"">First Example</a> we reached the point where we launched a second Eclipse |
| instance (but, of course, just defining a product you could have a standalone 3.x application) with a |
| view (called "My Library Tree Form") that allowed to manage the model. |
| </p> |
| <p> |
| <ol> |
| </ol> |
| </p> |
| <p> |
| <em>Preparing for a pure e4 Application</em> |
| </p> |
| <p> |
| What we will do now is starting from the previous step and create an e4 Application (on top of |
| the previous plug-ins) that gets to the same result, but now with a pure e4 Part. |
| </p> |
| <p> |
| In order to do this we need to export the <em>"org.eclipse.emf.parsley.examples.firstexample"</em> package from the first plug-in. |
| </p> |
| <p> |
| <ol> |
| </ol> |
| </p> |
| <p> |
| <em>Create an e4 Application</em> |
| </p> |
| <p> |
| Now let's create a new, empty, e4 application, e.g. <em>"org.eclipse.emf.parsley.examples.firstexample.application"</em> |
| (you can find details on how to create e4 applications in <a href="http://www.rcp-vision.com/?p=4694&lang=en">our |
| tutorials</a>). |
| </p> |
| <p> |
| Create a Part and ensure that the application starts. |
| </p> |
| <p> |
| <ol> |
| </ol> |
| </p> |
| <p> |
| <em>Using a TreeComposite into an e4 Part</em> |
| </p> |
| <p> |
| In the just created plug-in we need dependencies from the previous plug-ins: so open the <em>org.eclipse.emf.parsley.examples.firstexample.application/MANIFEST.MF</em> file, go to <em>Dependencies</em> |
| tab and add the three previous plug-ins. Add also <em>"org.eclipse.emf.parsley"</em> plug-in. |
| Don't forget to add the previous, and the required plug-ins, also to the Product. |
| </p> |
| <p> |
| Open the Part java class and make the following changes: |
| </p> |
| <p> |
| <div class="literallayout"> |
| <div class="incode"> |
| <p class="code"> |
| <span class="comment">// Use these imports during Organizing Imports operation<br/> |
| </span><span class="keyword">import</span> org.eclipse.emf.common.util.URI;<br/> |
| <span class="keyword">import</span> org.eclipse.emf.ecore.resource.Resource;<br/> |
| <span class="keyword">import</span> org.eclipse.swt.widgets.Composite;<br/> |
| <br/> |
| <span class="comment">// The part implements IMenuListener for context menu handling<br/> |
| </span><span class="keyword">public</span> <span class="keyword">class</span> MyEclipse4Part {<br/> |
| <br/> |
| <span class="comment">//the EMF Parley composite for showing a tree and a detail form<br/> |
| </span> <span class="keyword">private</span> TreeFormComposite treeFormComposite;<br/> |
| <span class="comment">//the EMF Resource<br/> |
| </span> <span class="keyword">private</span> Resource resource;<br/> |
| <span class="comment">//URI for EMF Resource<br/> |
| </span> <span class="keyword">private</span> URI uri = URI.createFileURI(System.getProperty(<span class="string">"user.home"</span>)<br/> |
| + <span class="string">"/MyLibrary.library"</span>);<br/> |
| <br/> |
| </p> |
| </div> |
| </div> |
| </p> |
| <p> |
| Modify the <em>@PostConstruct</em> method with this code: |
| </p> |
| <p> |
| <div class="literallayout"> |
| <div class="incode"> |
| <p class="code"> |
| @PostConstruct<br/> |
| <span class="keyword">public</span> <span class="keyword">void</span> postConstruct(Composite parent) {<br/> |
| <span class="comment">// Guice injector<br/> |
| </span> <span class="keyword">private</span> Injector injector = FirstexampleInjectorProvider.getInjector();<br/> |
| <br/> |
| <span class="comment">// The EditingDomain is needed for context menu and drag and drop<br/> |
| </span> EditingDomain editingDomain = injector.getInstance(EditingDomain.<span class="keyword">class</span>);<br/> |
| <br/> |
| ResourceLoader resourceLoader = injector.getInstance(ResourceLoader.<span class="keyword">class</span>);<br/> |
| <span class="comment">//load the resource<br/> |
| </span> resource = resourceLoader.getResource(editingDomain, uri).getResource();<br/> |
| <br/> |
| TreeFormFactory treeFormFactory = injector.getInstance(TreeFormFactory.<span class="keyword">class</span>);<br/> |
| <span class="comment">//create the tree-form composite<br/> |
| </span> treeFormComposite = treeFormFactory.createTreeFormComposite(parent, SWT.BORDER);<br/> |
| <br/> |
| <span class="comment">// Guice injected viewer context menu helper<br/> |
| </span> ViewerContextMenuHelper contextMenuHelper = injector.getInstance(ViewerContextMenuHelper.<span class="keyword">class</span>);<br/> |
| <span class="comment">// Guice injected viewer drag and drop helper<br/> |
| </span> ViewerDragAndDropHelper dragAndDropHelper = injector.getInstance(ViewerDragAndDropHelper.<span class="keyword">class</span>);<br/> |
| <br/> |
| <span class="comment">// set context menu and drag and drop<br/> |
| </span> contextMenuHelper.addViewerContextMenu(treeFormComposite.getViewer(), editingDomain);<br/> |
| dragAndDropHelper.addDragAndDrop(treeFormComposite.getViewer(), editingDomain);<br/> |
| <br/> |
| <span class="comment">//update the composite<br/> |
| </span> treeFormComposite.update(resource);<br/> |
| }<br/> |
| </p> |
| </div> |
| </div> |
| </p> |
| <p> |
| The Google Guice Injector (not to be confused with the Eclipse e4 Injector) is retrieved |
| using the injector provider class generated by the DSL compiler |
| (see also <a href="01-Overview.html#InjectorProvider" title="Go to "Obtaining the Injector"">Obtaining the Injector</a>). |
| </p> |
| <p> |
| If you now run the application you will be able to manage the model: |
| </p> |
| <p> |
| <div class="image" > |
| <img src="images/07-eclipse4-part.png" class=" " |
| /> |
| <div class="caption"> |
| </div> |
| </div> |
| </p> |
| <p> |
| but you will notice that it is not possible to persist the changes to the model. |
| </p> |
| <p> |
| <ol> |
| </ol> |
| </p> |
| <p> |
| <em>Adding the dirty state and Save command</em> |
| </p> |
| <p> |
| In order to allow persisting the model changes we have to add the dirty state handling to the part and |
| the Save command to the application. |
| Let's start with adding the following attribute to the part |
| </p> |
| <p> |
| <div class="literallayout"> |
| <div class="incode"> |
| <p class="code"> |
| @Inject<br/> |
| MDirtyable dirty;<br/> |
| </p> |
| </div> |
| </div> |
| </p> |
| <p> |
| add to <em>@PostConstruct</em> method the following code in order to update the dirty state |
| </p> |
| <p> |
| <div class="literallayout"> |
| <div class="incode"> |
| <p class="code"> |
| editingDomain.getCommandStack().addCommandStackListener(<br/> |
| <span class="keyword">new</span> CommandStackListener() {<br/> |
| <span class="keyword">public</span> <span class="keyword">void</span> commandStackChanged(EventObject event) {<br/> |
| <span class="keyword">if</span> (dirty != null)<br/> |
| dirty.setDirty(<span class="keyword">true</span>);<br/> |
| }<br/> |
| });<br/> |
| </p> |
| </div> |
| </div> |
| </p> |
| <p> |
| and add the <em>@Persist</em> method, which will be called when the part is saved |
| </p> |
| <p> |
| <div class="literallayout"> |
| <div class="incode"> |
| <p class="code"> |
| @Persist<br/> |
| <span class="keyword">public</span> <span class="keyword">void</span> save(MDirtyable dirty) <span class="keyword">throws</span> IOException {<br/> |
| resource.save(null);<br/> |
| <span class="keyword">if</span> (dirty != null) {<br/> |
| dirty.setDirty(<span class="keyword">false</span>);<br/> |
| }<br/> |
| }<br/> |
| </p> |
| </div> |
| </div> |
| </p> |
| <p> |
| and, in the end, add the <em>Save</em> handler along with the correspondent <em>Command</em> and <em>Menu</em> |
| (you can find how to create handlers, commands and menus in an e4 applications in <a href="http://www.rcp-vision.com/?p=4972&lang=en">our |
| tutorials</a>) |
| </p> |
| <p> |
| <div class="literallayout"> |
| <div class="incode"> |
| <p class="code"> |
| <span class="keyword">import</span> javax.inject.Named;<br/> |
| <br/> |
| <span class="keyword">public</span> <span class="keyword">class</span> SaveHandler {<br/> |
| <br/> |
| @Execute<br/> |
| <span class="keyword">void</span> execute(EPartService partService, @Named(IServiceConstants.ACTIVE_PART) MPart part) {<br/> |
| partService.savePart(part, <span class="keyword">false</span>);<br/> |
| }<br/> |
| }<br/> |
| </p> |
| </div> |
| </div> |
| </p> |
| <a name="RAP"></a> |
| <h2>RAP</h2> |
| <p> |
| As you may know <a href="http://eclipse.org/rap/">RAP (Remote Application Platform)</a> is a technology that allows you to run an Eclipse RCP application over the web. |
| </p> |
| <p> |
| In order to obtain this goal you have to setup a specific RAP Target Platform, for instance the one that RAP itself provides once you install it. |
| </p> |
| <p> |
| However when you want to use an Eclipse RCP framework over the RAP Platform, you generally have to deal with |
| dependencies, since not all Eclipse frameworks are ready-to-use with RAP, especially those related with the SWT layer. |
| </p> |
| <p> |
| EMF Parsley provides a proper RAP Target Platform that allows you to start leveraging Parsley potentials to the web the same way you have |
| learned to do with desktop (RCP) development. |
| </p> |
| <p> |
| <ol> |
| </ol> |
| </p> |
| <p> |
| <em>Installing the RAP Tools</em> |
| </p> |
| <p> |
| To begin with, you need to install the RAP Tools into the IDE. |
| This can be accomplished with the following steps: |
| <ol> |
| <li> |
| Help -> Install New Software ... |
| </li> |
| <li> |
| select the main Eclipse Update site |
| </li> |
| <li> |
| expand category "Web, XML, Java EE and OSGi Enterprise Development" |
| </li> |
| <li> |
| select "RAP Tools" and complete the installation, restarting the IDE at the end |
| </li> |
| <li> |
| after IDE restarts just close the Welcome page |
| </li> |
| </ol> |
| </p> |
| <p> |
| <ol> |
| </ol> |
| </p> |
| <p> |
| <em>Setup the EMF Parsley RAP Target Platform</em> |
| </p> |
| <p> |
| After having installed EMF Parsley as described <a href="https://www.eclipse.org/emf-parsley/download.html">here</a> and |
| created a new workspace, you can setup the EMF Parsley RAP Target Platform in the following way: |
| <ol> |
| <li> |
| File -> New... -> Example... |
| </li> |
| <li> |
| from Category "EMF Parsley Examples", select "EMF Parsley RAP Target Platform Example" |
| </li> |
| <li> |
| press Next and Finish |
| </li> |
| <li> |
| open the Target Definition file <em>emf-parsely-rap.target</em> |
| </li> |
| <li> |
| wait until the "Resolving Target Definition" job is done (check the status bar) |
| </li> |
| <li> |
| when finished, click on hyperlink "Set as Target Platform" |
| </li> |
| </ol> |
| </p> |
| <p> |
| You will end up with a RAP-enabled workspace, enhanced by EMF and Parsley! |
| </p> |
| <p> |
| <ol> |
| </ol> |
| </p> |
| <p> |
| <em>Running the Parsley RAP UI Example</em> |
| </p> |
| <p> |
| Here is the fastest way to get a working web application with all the stuff put togheter: |
| <ol> |
| <li> |
| File -> New... -> Example... |
| </li> |
| <li> |
| from Category "EMF Parsley Examples", select "EMF Parsley RAP Example" |
| </li> |
| <li> |
| press Next and Finish |
| </li> |
| <li> |
| expand plug-in <em>"org.eclipse.emf.parsley.examples.rap.ui"</em> |
| </li> |
| <li> |
| right-click "Emf_Parsley_RAP_UI_Example.launch" and click "Run as" "Emf_Parsley_RAP_UI_Example" |
| </li> |
| </ol> |
| </p> |
| <p> |
| What you will get is a web application that allows you to interact with the model instance as you would |
| do in a desktop (RCP) environment. |
| </p> |
| <p> |
| <div class="image" > |
| <img src="images/08-rap-ui-example-running.png" class=" " |
| /> |
| <div class="caption"> |
| </div> |
| </div> |
| </p> |
| <p> |
| In this web application you can see two views: |
| <ul> |
| <li> |
| the one on the left is a read-only view; it just reflects the model content, but it does not react to changes (the classic Eclipse dirty indicator is not triggered |
| by changes) and you are not able to save. Its model is created in class <em>org.eclipse.emf.parsley.examples.rap.ui.GuiceModule.CustomResourceManager</em> |
| and is not persisted |
| </li> |
| <li> |
| the view on the right is instead a Saveable view and therefore it not only triggers the dirty state after |
| a change, but also allows you to save the modifications with the automatic dirty state reset. Its model |
| is persisted in file <em>System.getProperty("java.io.tmpdir")+"/My.model")</em> |
| </li> |
| </ul> |
| </p> |
| <p> |
| Of course, since this is a web application, you can also open a browser on another pc or device on the same network and type the address, |
| replacing 127.0.0.1 with the IP of the machine where the application was launched. |
| </p> |
| <p> |
| <ol> |
| </ol> |
| </p> |
| <p> |
| <em>Running the Parsley RAP CDO Example</em> |
| </p> |
| <p> |
| The EMF default XMI persistence is certainly very handy to start with, but as soon as you want a more |
| production-ready EMF persistence architecture, well, <a href="http://wiki.eclipse.org/CDO">CDO</a> is for sure the way to go. |
| In fact with CDO you basically have an EMF model instance shared between clients, that also allows the |
| clients to be synchronized with the model changes. |
| </p> |
| <p> |
| In this example, in order to keep things simple, we will use CDO with an in-memory store (MEMStore) whose contents will be lost once the server is stopped. |
| However CDO can be configured for usage with RDBMS, Object-oriented or NO-SQL databases (see <a href="http://eclipse.org/cdo/documentation/">here</a> for details) |
| </p> |
| <p> |
| To start with we need a CDO Server running and we can obtain it with an example plugin that can be used |
| both in an RCP and in a RAP workspace. |
| </p> |
| <p> |
| <ol> |
| <li> |
| File -> New... -> Example... |
| </li> |
| <li> |
| from Category "EMF Parsley Examples", select "EMF Parsley CDO Server Example" |
| </li> |
| <li> |
| press Next and Finish |
| </li> |
| <li> |
| expand plug-in <em>"org.eclipse.emf.parsley.examples.cdo.server"</em> |
| </li> |
| <li> |
| right-click "CDOServerExample.launch" and click "Run as" "CDOServerExample" |
| </li> |
| <li> |
| a message on the Console <em>"Repository[demo] started!"</em> informs that the CDO Server instance |
| is started! |
| </li> |
| </ol> |
| </p> |
| <p> |
| Now we can create the web application that will use the CDO server just started. |
| </p> |
| <p> |
| <ol> |
| <li> |
| File -> New... -> Example... |
| </li> |
| <li> |
| from Category "EMF Parsley Examples", select "EMF Parsley RAP CDO Example" |
| </li> |
| <li> |
| press Next and Finish |
| </li> |
| </ol> |
| </p> |
| <p> |
| The plug-in projects created are: |
| </p> |
| <p> |
| <ul> |
| <li> |
| the Model (org.eclipse.emf.parsley.examples.cdo.model) |
| </li> |
| <li> |
| a Parsley plug-in with a TreeForm (org.eclipse.emf.parsley.examples.cdo.treeform) |
| </li> |
| <li> |
| the webapp (org.eclipse.emf.parsley.examples.cdo.rap) |
| </li> |
| </ul> |
| </p> |
| <p> |
| Then let's start the application |
| </p> |
| <p> |
| <ol> |
| <li> |
| expand plug-in <em>"org.eclipse.emf.parsley.examples.cdo.rap"</em> |
| </li> |
| <li> |
| right-click "EMF-Parsley_Library_RAP.launch" and click "Run as" "EMF-Parsley_Library_RAP" |
| </li> |
| </ol> |
| </p> |
| <p> |
| If you happen to see this |
| </p> |
| <p> |
| <div class="image" > |
| <img src="images/08-rap-refresh.png" class=" " |
| /> |
| <div class="caption"> |
| </div> |
| </div> |
| </p> |
| <p> |
| just press the refresh button and should see the following |
| </p> |
| <p> |
| <div class="image" > |
| <img src="images/08-rap-cdo-1.png" class=" " |
| /> |
| <div class="caption"> |
| </div> |
| </div> |
| </p> |
| <p> |
| Now feel free to open the same address from more browsers window (yes, on different machines or devices, possibly) |
| and see the power of this technology stack at work! |
| </p> |
| <p> |
| <div class="image" > |
| <img src="images/08-rap-cdo-2.png" class=" " |
| /> |
| <div class="caption"> |
| </div> |
| </div> |
| </p> |
| </body> |
| </html> |