View Javadoc
1   package org.opentrafficsim.web;
2   
3   import java.net.URL;
4   import java.util.ArrayList;
5   import java.util.Collections;
6   import java.util.LinkedHashMap;
7   import java.util.List;
8   import java.util.Map;
9   
10  import org.djunits.unit.Unit;
11  import org.djunits.value.vdouble.scalar.Duration;
12  import org.djunits.value.vdouble.scalar.Time;
13  import org.djunits.value.vdouble.scalar.base.DoubleScalar;
14  import org.djunits.value.vfloat.scalar.base.FloatScalar;
15  import org.djutils.io.ResourceResolver;
16  import org.eclipse.jetty.ee10.servlet.SessionHandler;
17  import org.eclipse.jetty.io.Content;
18  import org.eclipse.jetty.server.Handler;
19  import org.eclipse.jetty.server.Request;
20  import org.eclipse.jetty.server.Response;
21  import org.eclipse.jetty.server.Server;
22  import org.eclipse.jetty.server.handler.ContextHandler;
23  import org.eclipse.jetty.server.handler.ContextHandlerCollection;
24  import org.eclipse.jetty.server.handler.ResourceHandler;
25  import org.eclipse.jetty.session.DefaultSessionCache;
26  import org.eclipse.jetty.session.DefaultSessionIdManager;
27  import org.eclipse.jetty.session.NullSessionDataStore;
28  import org.eclipse.jetty.session.SessionCache;
29  import org.eclipse.jetty.session.SessionDataStore;
30  import org.eclipse.jetty.util.Callback;
31  import org.eclipse.jetty.util.Fields;
32  import org.opentrafficsim.animation.DefaultAnimationFactory;
33  import org.opentrafficsim.base.logger.Logger;
34  import org.opentrafficsim.core.dsol.OtsAnimator;
35  import org.opentrafficsim.core.dsol.OtsModelInterface;
36  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
37  import org.opentrafficsim.core.perception.HistoryManagerDevs;
38  import org.opentrafficsim.web.test.CircularRoadModel;
39  import org.opentrafficsim.web.test.TJunctionModel;
40  
41  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameter;
42  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterBoolean;
43  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterDistContinuousSelection;
44  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterDistDiscreteSelection;
45  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterDouble;
46  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterDoubleScalar;
47  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterFloat;
48  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterFloatScalar;
49  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterInteger;
50  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterLong;
51  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterMap;
52  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterSelectionList;
53  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterSelectionMap;
54  import nl.tudelft.simulation.dsol.model.inputparameters.InputParameterString;
55  
56  /**
57   * DSOLWebServer.java.
58   * <p>
59   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
60   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
61   * </p>
62   * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
63   */
64  public class TestDemoServer
65  {
66      /** the map of sessionIds to OtsModelInterface that handles the animation and updates for the started model. */
67      final Map<String, OtsModelInterface> sessionModelMap = new LinkedHashMap<>();
68  
69      /** the map of sessionIds to OTSWebModel that handles the animation and updates for the started model. */
70      final Map<String, OtsWebModel> sessionWebModelMap = new LinkedHashMap<>();
71  
72      /**
73       * Run a SuperDemo OTS Web server.
74       * @param args not used
75       * @throws Exception on Jetty error
76       */
77      public static void main(final String[] args) throws Exception
78      {
79          new TestDemoServer();
80      }
81  
82      /**
83       * Constructor.
84       * @throws Exception in case jetty crashes
85       */
86      public TestDemoServer() throws Exception
87      {
88          new ServerThread().start();
89      }
90  
91      /** Handle in separate thread to avoid 'lock' of the main application. */
92      class ServerThread extends Thread
93      {
94          /**
95           * Constructor.
96           */
97          public ServerThread()
98          {
99              //
100         }
101 
102         @Override
103         public void run()
104         {
105             Server server = new Server(8080);
106             ResourceHandler resourceHandler = new MyResourceHandler();
107 
108             // root folder; to work in Eclipse, as an external jar, and in an embedded jar
109             URL homeFolder = ResourceResolver.resolve("/resources/home").asUrl();
110             String webRoot = homeFolder.toExternalForm();
111             Logger.ots().trace("webRoot is " + webRoot);
112 
113             resourceHandler.setDirAllowed(true);
114             resourceHandler.setWelcomeFiles(new String[] {"testdemo.html"});
115             resourceHandler.setBaseResourceAsString(webRoot);
116 
117             DefaultSessionIdManager idManager = new DefaultSessionIdManager(server);
118             idManager.setWorkerName("testDemoServer");
119             server.addBean(idManager, true);
120 
121             SessionHandler sessionHandler = new SessionHandler();
122             SessionCache sessionCache = new DefaultSessionCache(sessionHandler);
123             SessionDataStore sessionDataStore = new NullSessionDataStore();
124             sessionCache.setSessionDataStore(sessionDataStore);
125             sessionHandler.setSessionCache(sessionCache);
126 
127             ContextHandler handler1 = new ContextHandler(resourceHandler, "/");
128             ContextHandler handler2 = new ContextHandler(sessionHandler, "/");
129             ContextHandler handler3 = new ContextHandler(new XHRHandler(TestDemoServer.this), "/");
130             ContextHandlerCollection handlers = new ContextHandlerCollection();
131             handlers.setHandlers(new Handler[] {handler1, handler2, handler3});
132             handlers.mapContexts();
133             server.setHandler(handlers);
134 
135             try
136             {
137                 server.start();
138                 server.join();
139             }
140             catch (Exception exception)
141             {
142                 exception.printStackTrace();
143             }
144         }
145     }
146 
147     /** Resource handler. */
148     class MyResourceHandler extends ResourceHandler
149     {
150         /**
151          * Constructor.
152          */
153         MyResourceHandler()
154         {
155             //
156         }
157 
158         @Override
159         public boolean handle(final Request request, final Response response, final Callback callback) throws Exception
160         {
161 
162             // https://jetty.org/docs/jetty/12.1/programming-guide/migration/11-to-12.html#api-changes
163 
164             String target = request.getHttpURI().getPathQuery();
165 
166             if (target.startsWith("/parameters.html"))
167             {
168                 Fields fields = Request.getParameters(request);
169                 String modelId = fields.getValue("model");
170                 String sessionId = fields.getValue("sessionId");
171                 if (!TestDemoServer.this.sessionModelMap.containsKey(sessionId))
172                 {
173                     Logger.ots().trace("parameters: " + modelId);
174                     OtsAnimator simulator = new OtsAnimator("TestDemoServer");
175                     simulator.setAnimation(false);
176                     OtsModelInterface model = null;
177                     if (modelId.toLowerCase().contains("circularroad"))
178                         model = new CircularRoadModel(simulator);
179                     else if (modelId.toLowerCase().contains("tjunction"))
180                         model = new TJunctionModel(simulator);
181                     if (model != null)
182                         TestDemoServer.this.sessionModelMap.put(sessionId, model);
183                     else
184                         Logger.ots().error("Could not find model " + modelId);
185                 }
186             }
187 
188             if (target.startsWith("/model.html"))
189             {
190                 Fields fields = Request.getParameters(request);
191                 String modelId = fields.getValue("model");
192                 String sessionId = fields.getValue("sessionId");
193                 if (TestDemoServer.this.sessionModelMap.containsKey(sessionId)
194                         && !TestDemoServer.this.sessionWebModelMap.containsKey(sessionId))
195                 {
196                     Logger.ots().trace("startModel: " + modelId);
197                     OtsModelInterface model = TestDemoServer.this.sessionModelMap.get(sessionId);
198                     OtsSimulatorInterface simulator = model.getSimulator();
199                     try
200                     {
201                         simulator.initialize(Time.ZERO, Duration.ZERO, Duration.ofSI(3600.0), model,
202                                 HistoryManagerDevs.noHistory(simulator));
203                         OtsWebModel webModel = new OtsWebModel(model.getShortName(), simulator);
204                         TestDemoServer.this.sessionWebModelMap.put(sessionId, webModel);
205                         DefaultAnimationFactory.animateNetwork(model.getNetwork(), model.getNetwork().getSimulator(),
206                                 webModel.getAnimationPanel().getGtuColorerManager(), Collections.emptyMap());
207                     }
208                     catch (Exception exception)
209                     {
210                         exception.printStackTrace();
211                     }
212                 }
213             }
214 
215             // handle whatever needs to be done...
216             return super.handle(request, response, callback);
217         }
218     }
219 
220     /**
221      * Answer handles the events from the web-based user interface for a demo. <br>
222      * <br>
223      * Copyright (c) 2003-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
224      * See for project information <a href="https://www.simulation.tudelft.nl/" target="_blank">www.simulation.tudelft.nl</a>.
225      * The source code and binary code of this software is proprietary information of Delft University of Technology.
226      * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
227      */
228     public static class XHRHandler extends Handler.Abstract
229     {
230         /** web server for callback of actions. */
231         private final TestDemoServer webServer;
232 
233         /**
234          * Create the handler for Servlet requests.
235          * @param webServer web server for callback of actions
236          */
237         public XHRHandler(final TestDemoServer webServer)
238         {
239             this.webServer = webServer;
240         }
241 
242         /** {@inheritDoc} */
243         @Override
244         public boolean handle(final Request request, final Response response, final Callback callback) throws Exception
245         {
246 
247             // https://jetty.org/docs/jetty/12.1/programming-guide/migration/11-to-12.html#api-changes
248 
249             Fields fields = Request.getParameters(request);
250             String sessionId = fields.getValue("sessionId");
251             if (sessionId != null)
252             {
253                 if (this.webServer.sessionWebModelMap.containsKey(sessionId))
254                 {
255                     boolean handled = this.webServer.sessionWebModelMap.get(sessionId).handle(request, response, callback);
256                     if (handled)
257                     {
258                         return true;
259                     }
260                 }
261                 else if (this.webServer.sessionModelMap.containsKey(sessionId))
262                 {
263                     OtsModelInterface model = this.webServer.sessionModelMap.get(sessionId);
264                     String answer = "<message>ok</message>";
265 
266                     String message = fields.getValue("message");
267                     if (message != null)
268                     {
269                         String[] parts = message.split("\\|");
270                         String command = parts[0];
271 
272                         switch (command)
273                         {
274                             case "getTitle":
275                             {
276                                 answer = "<title>" + model.getShortName() + "</title>";
277                                 break;
278                             }
279 
280                             case "getParameterMap":
281                             {
282                                 answer = makeParameterMap(model);
283                                 break;
284                             }
285 
286                             case "setParameters":
287                             {
288                                 answer = setParameters(model, message);
289                                 break;
290                             }
291 
292                             default:
293                             {
294                                 Logger.ots().error("Got unknown message from client: {}", command);
295                                 answer = "<message>" + request.getAttribute("message") + "</message>";
296                                 break;
297                             }
298                         }
299                     }
300 
301                     Content.Sink.write(response, true, answer, callback);
302 
303                     return true; // handled
304                 }
305             }
306 
307             return false;
308         }
309 
310         /**
311          * Make the parameter set that can be interpreted by the parameters.html page.
312          * @param model the model with parameters
313          * @return an XML string with the parameters
314          */
315         private String makeParameterMap(final OtsModelInterface model)
316         {
317             StringBuffer answer = new StringBuffer();
318             answer.append("<parameters>\n");
319             InputParameterMap inputParameterMap = model.getInputParameterMap();
320             for (InputParameter<?, ?> tab : inputParameterMap.getSortedSet())
321             {
322                 if (!(tab instanceof InputParameterMap))
323                 {
324                     Logger.ots().error("Input parameter {} cannot be displayed in a tab", tab.getShortName());
325                 }
326                 else
327                 {
328                     answer.append("<tab>" + tab.getDescription() + "</tab>\n");
329                     InputParameterMap tabbedMap = (InputParameterMap) tab;
330                     for (InputParameter<?, ?> parameter : tabbedMap.getSortedSet())
331                     {
332                         addParameterField(answer, parameter);
333                     }
334                 }
335             }
336             answer.append("</parameters>\n");
337             return answer.toString();
338         }
339 
340         /**
341          * Add the right type of field for this parameter to the string buffer.
342          * @param answer the buffer to add the XML-info for the parameter
343          * @param parameter the input parameter to display
344          */
345         public void addParameterField(final StringBuffer answer, final InputParameter<?, ?> parameter)
346         {
347             if (parameter instanceof InputParameterDouble)
348             {
349                 InputParameterDouble pd = (InputParameterDouble) parameter;
350                 answer.append("<double key='" + pd.getExtendedKey() + "' name='" + pd.getShortName() + "' description='"
351                         + pd.getDescription() + "'>" + pd.getValue() + "</double>\n");
352             }
353             else if (parameter instanceof InputParameterFloat)
354             {
355                 InputParameterFloat pf = (InputParameterFloat) parameter;
356                 answer.append("<float key='" + pf.getExtendedKey() + "' name='" + pf.getShortName() + "' description='"
357                         + pf.getDescription() + "'>" + pf.getValue() + "</float>\n");
358             }
359             else if (parameter instanceof InputParameterBoolean)
360             {
361                 InputParameterBoolean pb = (InputParameterBoolean) parameter;
362                 answer.append("<boolean key='" + pb.getExtendedKey() + "' name='" + pb.getShortName() + "' description='"
363                         + pb.getDescription() + "'>" + pb.getValue() + "</boolean>\n");
364             }
365             else if (parameter instanceof InputParameterLong)
366             {
367                 InputParameterLong pl = (InputParameterLong) parameter;
368                 answer.append("<long key='" + pl.getExtendedKey() + "' name='" + pl.getShortName() + "' description='"
369                         + pl.getDescription() + "'>" + pl.getValue() + "</long>\n");
370             }
371             else if (parameter instanceof InputParameterInteger)
372             {
373                 InputParameterInteger pi = (InputParameterInteger) parameter;
374                 answer.append("<integer key='" + pi.getExtendedKey() + "' name='" + pi.getShortName() + "' description='"
375                         + pi.getDescription() + "'>" + pi.getValue() + "</integer>\n");
376             }
377             else if (parameter instanceof InputParameterString)
378             {
379                 InputParameterString ps = (InputParameterString) parameter;
380                 answer.append("<string key='" + ps.getExtendedKey() + "' name='" + ps.getShortName() + "' description='"
381                         + ps.getDescription() + "'>" + ps.getValue() + "</string>\n");
382             }
383             else if (parameter instanceof InputParameterDoubleScalar)
384             {
385                 InputParameterDoubleScalar<?, ?> pds = (InputParameterDoubleScalar<?, ?>) parameter;
386                 String val = getValueInUnit(pds);
387                 List<String> units = getUnits(pds);
388                 answer.append("<doubleScalar key='" + pds.getExtendedKey() + "' name='" + pds.getShortName() + "' description='"
389                         + pds.getDescription() + "'><value>" + val + "</value>\n");
390                 for (String unit : units)
391                 {
392                     Unit<?> unitValue = pds.getUnitParameter().getOptions().get(unit);
393                     if (unitValue.equals(pds.getUnitParameter().getValue()))
394                         answer.append("<unit chosen='true'>" + unit + "</unit>\n");
395                     else
396                         answer.append("<unit chosen='false'>" + unit + "</unit>\n");
397                 }
398                 answer.append("</doubleScalar>\n");
399             }
400             else if (parameter instanceof InputParameterFloatScalar)
401             {
402                 InputParameterFloatScalar<?, ?> pds = (InputParameterFloatScalar<?, ?>) parameter;
403                 String val = getValueInUnit(pds);
404                 List<String> units = getUnits(pds);
405                 answer.append("<floatScalar key='" + pds.getExtendedKey() + "' name='" + pds.getShortName() + "' description='"
406                         + pds.getDescription() + "'><value>" + val + "</value>\n");
407                 for (String unit : units)
408                 {
409                     Unit<?> unitValue = pds.getUnitParameter().getOptions().get(unit);
410                     if (unitValue.equals(pds.getUnitParameter().getValue()))
411                         answer.append("<unit chosen='true'>" + unit + "</unit>\n");
412                     else
413                         answer.append("<unit chosen='false'>" + unit + "</unit>\n");
414                 }
415                 answer.append("</floatScalar>\n");
416             }
417             else if (parameter instanceof InputParameterSelectionList<?>)
418             {
419                 // TODO InputParameterSelectionList
420             }
421             else if (parameter instanceof InputParameterDistDiscreteSelection)
422             {
423                 // TODO InputParameterSelectionList
424             }
425             else if (parameter instanceof InputParameterDistContinuousSelection)
426             {
427                 // TODO InputParameterDistContinuousSelection
428             }
429             else if (parameter instanceof InputParameterSelectionMap<?, ?>)
430             {
431                 // TODO InputParameterSelectionMap
432             }
433         }
434 
435         /**
436          * @param parameter double scalar input parameter
437          * @return default value in the unit
438          */
439         private <U extends Unit<U>,
440                 T extends DoubleScalar<U, T>> String getValueInUnit(final InputParameterDoubleScalar<U, T> parameter)
441         {
442             return "" + parameter.getDefaultTypedValue().getInUnit(parameter.getDefaultTypedValue().getDisplayUnit());
443         }
444 
445         /**
446          * @param parameter double scalar input parameter
447          * @return abbreviations for the units
448          */
449         private <U extends Unit<U>,
450                 T extends DoubleScalar<U, T>> List<String> getUnits(final InputParameterDoubleScalar<U, T> parameter)
451         {
452             List<String> unitList = new ArrayList<>();
453             for (String option : parameter.getUnitParameter().getOptions().keySet())
454             {
455                 unitList.add(option.toString());
456             }
457             return unitList;
458         }
459 
460         /**
461          * @param parameter double scalar input parameter
462          * @return default value in the unit
463          */
464         private <U extends Unit<U>,
465                 T extends FloatScalar<U, T>> String getValueInUnit(final InputParameterFloatScalar<U, T> parameter)
466         {
467             return "" + parameter.getDefaultTypedValue().getInUnit(parameter.getDefaultTypedValue().getDisplayUnit());
468         }
469 
470         /**
471          * @param parameter double scalar input parameter
472          * @return abbreviations for the units
473          */
474         private <U extends Unit<U>,
475                 T extends FloatScalar<U, T>> List<String> getUnits(final InputParameterFloatScalar<U, T> parameter)
476         {
477             List<String> unitList = new ArrayList<>();
478             for (String option : parameter.getUnitParameter().getOptions().keySet())
479             {
480                 unitList.add(option.toString());
481             }
482             return unitList;
483         }
484 
485         /**
486          * Make the parameter set that can be interpreted by the parameters.html page.
487          * @param model the model with parameters
488          * @param message the key-value pairs of the set parameters
489          * @return the errors if they are detected. If none, errors is set to "OK"
490          */
491         private String setParameters(final OtsModelInterface model, final String message)
492         {
493             String errors = "OK";
494             InputParameterMap inputParameters = model.getInputParameterMap();
495             String[] parts = message.split("\\|");
496             Map<String, String> unitMap = new LinkedHashMap<>();
497             for (int i = 1; i < parts.length - 3; i += 3)
498             {
499                 String id = parts[i].trim().replaceFirst("model.", "");
500                 String type = parts[i + 1].trim();
501                 String val = parts[i + 2].trim();
502                 if (type.equals("UNIT"))
503                 {
504                     unitMap.put(id, val);
505                 }
506             }
507             for (int i = 1; i < parts.length - 3; i += 3)
508             {
509                 String id = parts[i].trim().replaceFirst("model.", "");
510                 String type = parts[i + 1].trim();
511                 String val = parts[i + 2].trim();
512 
513                 try
514                 {
515                     if (type.equals("DOUBLE"))
516                     {
517                         InputParameterDouble param = (InputParameterDouble) inputParameters.get(id);
518                         param.setDoubleValue(Double.valueOf(val));
519                     }
520                     else if (type.equals("FLOAT"))
521                     {
522                         InputParameterFloat param = (InputParameterFloat) inputParameters.get(id);
523                         param.setFloatValue(Float.valueOf(val));
524                     }
525                     else if (type.equals("BOOLEAN"))
526                     {
527                         InputParameterBoolean param = (InputParameterBoolean) inputParameters.get(id);
528                         param.setBooleanValue(val.toUpperCase().startsWith("T"));
529                     }
530                     else if (type.equals("LONG"))
531                     {
532                         InputParameterLong param = (InputParameterLong) inputParameters.get(id);
533                         param.setLongValue(Long.valueOf(val));
534                     }
535                     else if (type.equals("INTEGER"))
536                     {
537                         InputParameterInteger param = (InputParameterInteger) inputParameters.get(id);
538                         param.setIntValue(Integer.valueOf(val));
539                     }
540                     else if (type.equals("STRING"))
541                     {
542                         InputParameterString param = (InputParameterString) inputParameters.get(id);
543                         param.setStringValue(val);
544                     }
545                     if (type.equals("DOUBLESCALAR"))
546                     {
547                         InputParameterDoubleScalar<?, ?> param = (InputParameterDoubleScalar<?, ?>) inputParameters.get(id);
548                         param.getDoubleParameter().setDoubleValue(Double.valueOf(val));
549                         String unitString = unitMap.get(id);
550                         if (unitString == null)
551                             Logger.ots().error("Could not find unit for Doublevalie parameter with id={}", id);
552                         else
553                         {
554                             Unit<?> unit = param.getUnitParameter().getOptions().get(unitString);
555                             if (unit == null)
556                                 Logger.ots().error("Could not find unit {} for Doublevalie parameter with id={}", unitString,
557                                         id);
558                             else
559                             {
560                                 param.getUnitParameter().setObjectValue(unit);
561                                 param.setCalculatedValue(); // it will retrieve the set double value and unit
562                             }
563                         }
564                     }
565                 }
566                 catch (Exception exception)
567                 {
568                     if (errors.equals("OK"))
569                         errors = "ERRORS IN INPUT VALUES:\n";
570                     errors += "Field " + id + ": " + exception.getMessage() + "\n";
571                 }
572             }
573             return errors;
574         }
575 
576     }
577 
578 }