/*
* This file is part of the openSCADA project
* Copyright (C) 2006-2011 TH4 SYSTEMS GmbH (http://th4-systems.com)
*
* openSCADA is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3
* only, as published by the Free Software Foundation.
*
* openSCADA is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License version 3 for more details
* (a copy is included in the LICENSE file that accompanied this code).
*
* You should have received a copy of the GNU Lesser General Public License
* version 3 along with openSCADA. If not, see
* <http://opensource.org/licenses/lgpl-3.0.html> for a copy of the LGPLv3 License.
*/
package org.openscada.vi.ui.draw2d;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.IConsoleManager;
import org.eclipse.ui.console.MessageConsole;
import org.eclipse.ui.console.MessageConsoleStream;
import org.eclipse.ui.statushandlers.StatusManager;
import org.openscada.core.Variant;
import org.openscada.da.client.DataItemValue;
import org.openscada.ui.utils.status.StatusHelper;
import org.openscada.utils.script.ScriptExecutor;
import org.openscada.vi.model.VisualInterface.Primitive;
import org.openscada.vi.model.VisualInterface.Symbol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.Resources;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class SymbolController
{
private final static Logger logger = LoggerFactory.getLogger ( SymbolController.class );
private final SymbolController parentController;
private final Set<SymbolController> controllers = new LinkedHashSet<SymbolController> ();
private final ScriptExecutor onInit;
private final ScriptEngineManager engineManager;
private final ScriptEngine engine;
private final ScriptExecutor onDispose;
private final ScriptExecutor onUpdate;
private final Properties properties;
private final SymbolContext context;
private final ScriptContext scriptContext;
private final ClassLoader classLoader;
private final Map<String, Object> elements = new HashMap<String, Object> ();
private final Map<Primitive, Object> primitives = new HashMap<Primitive, Object> ();
private RegistrationManager registrationManager;
private final SymbolData symbolData;
private Map<String, DataValue> lastData;
private final Set<SummaryListener> summaryListeners = new LinkedHashSet<SummaryListener> ( 1 );
private final Map<String, Object> scriptObjects;
private MessageConsole console;
private MessageConsole createdConsole;
public SymbolController ( final Symbol symbol, final ClassLoader classLoader, final Map<String, String> properties, final Map<String, Object> scriptObjects ) throws Exception
{
this ( null, symbol, classLoader, properties, scriptObjects );
}
public SymbolController ( final SymbolController parentController, final Symbol symbol, final ClassLoader classLoader, final Map<String, String> properties, final Map<String, Object> scriptObjects ) throws Exception
{
this.parentController = parentController;
this.classLoader = classLoader;
this.symbolData = new SymbolData ( this );
this.registrationManager = new RegistrationManager ( this );
this.engineManager = new ScriptEngineManager ( classLoader );
if ( parentController != null )
{
this.properties = new Properties ( parentController.getProperties () );
}
else
{
this.properties = new Properties ();
}
for ( final Map.Entry<String, String> entry : symbol.getProperties ().entrySet () )
{
if ( entry.getValue () != null )
{
this.properties.put ( entry.getKey (), entry.getValue () );
}
}
for ( final Map.Entry<String, String> entry : properties.entrySet () )
{
if ( entry.getValue () != null )
{
this.properties.put ( entry.getKey (), entry.getValue () );
}
}
this.engine = this.engineManager.getEngineByName ( "JavaScript" );
this.context = new SymbolContext ( this );
if ( parentController != null )
{
parentController.addChild ( this );
}
this.scriptContext = this.engine.getContext ();
assignConsole ( this.scriptContext );
this.scriptContext.setAttribute ( "controller", this.context, ScriptContext.ENGINE_SCOPE );
this.scriptContext.setAttribute ( "data", this.symbolData, ScriptContext.ENGINE_SCOPE );
this.scriptContext.setAttribute ( "GSON", createJson (), ScriptContext.ENGINE_SCOPE );
this.scriptObjects = scriptObjects;
addScriptObjects ( scriptObjects );
if ( parentController != null )
{
addScriptObjects ( parentController.getScriptObjects () );
}
for ( final String module : symbol.getScriptModules () )
{
loadScript ( module );
}
this.onInit = new ScriptExecutor ( this.engine, symbol.getOnInit (), classLoader );
this.onDispose = new ScriptExecutor ( this.engine, symbol.getOnDispose (), classLoader );
this.onUpdate = new ScriptExecutor ( this.engine, symbol.getOnUpdate (), classLoader );
}
private void assignConsole ( final ScriptContext scriptContext )
{
this.console = createOrGetConsole ();
// scriptContext.setReader ( new InputStreamReader ( this.console.getInputStream () ) );
/* wrapping into a PrintWriter is necessary due to
* http://bugs.sun.com/view_bug.do?bug_id=6759414
* */
final MessageConsoleStream writerStream = this.console.newMessageStream ();
scriptContext.setWriter ( new PrintWriter ( new OutputStreamWriter ( writerStream ) ) );
final MessageConsoleStream errorStream = this.console.newMessageStream ();
errorStream.setColor ( Display.getDefault ().getSystemColor ( SWT.COLOR_RED ) );
scriptContext.setErrorWriter ( new PrintWriter ( new OutputStreamWriter ( errorStream ) ) );
}
private MessageConsole createOrGetConsole ()
{
if ( this.parentController != null && this.parentController.getConsole () != null )
{
return this.parentController.getConsole ();
}
final IConsoleManager manager = ConsolePlugin.getDefault ().getConsoleManager ();
final MessageConsole console = new MessageConsole ( "Symbol Debug Console", null, null, true );
manager.addConsoles ( new IConsole[] { console } );
this.createdConsole = console;
return console;
}
protected MessageConsole getConsole ()
{
if ( this.console != null )
{
return this.console;
}
else
{
return null;
}
}
private Gson createJson ()
{
return new GsonBuilder ().serializeNulls ().setDateFormat ( "yyyy-MM-dd hh:mm:ss.SSS" ).create ();
}
private void addScriptObjects ( final Map<String, Object> scriptObjects )
{
if ( scriptObjects != null )
{
for ( final Map.Entry<String, Object> entry : scriptObjects.entrySet () )
{
this.scriptContext.setAttribute ( entry.getKey (), entry.getValue (), ScriptContext.ENGINE_SCOPE );
}
}
}
public Map<String, Object> getScriptObjects ()
{
return this.scriptObjects;
}
private void loadScript ( final String module ) throws Exception
{
final String moduleSource = Resources.toString ( new URL ( module ), Charset.forName ( "UTF-8" ) );
new ScriptExecutor ( this.engine, moduleSource, this.classLoader ).execute ( this.scriptContext );
}
public void init () throws Exception
{
this.onInit.execute ( this.scriptContext );
for ( final SymbolController controller : this.controllers )
{
controller.init ();
}
}
public Properties getProperties ()
{
return this.properties;
}
protected void addChild ( final SymbolController controller )
{
this.controllers.add ( controller );
}
protected void removeChild ( final SymbolController controller )
{
this.controllers.remove ( controller );
}
public void dispose ()
{
if ( this.createdConsole != null )
{
ConsolePlugin.getDefault ().getConsoleManager ().removeConsoles ( new IConsole[] { this.createdConsole } );
this.createdConsole = null;
}
try
{
if ( this.onDispose != null )
{
this.onDispose.execute ( this.scriptContext );
}
}
catch ( final Exception e )
{
logger.warn ( "Failed to dispose", e );
}
if ( this.parentController != null )
{
this.parentController.removeChild ( this );
}
final ArrayList<SymbolController> controllers = new ArrayList<SymbolController> ( this.controllers );
for ( final SymbolController controller : controllers )
{
controller.dispose ();
}
this.controllers.clear ();
if ( this.registrationManager != null )
{
this.registrationManager.dispose ();
this.registrationManager = null;
}
}
public Object createProperties ( final String command, final String onCreateProperties, final Map<String, String> currentProperties ) throws Exception
{
final ScriptExecutor executor = new ScriptExecutor ( this.engine, onCreateProperties, this.classLoader );
final SimpleScriptContext currentContext = new SimpleScriptContext ();
final Bindings bindings = this.engine.createBindings ();
bindings.put ( "properties", currentProperties );
bindings.put ( "controller", this.context );
currentContext.setBindings ( bindings, ScriptContext.ENGINE_SCOPE );
return executor.execute ( currentContext );
}
public Object getElement ( final String name )
{
return this.elements.get ( name );
}
public Object getElement ( final Primitive primitive )
{
return this.primitives.get ( primitive );
}
public void addRawElement ( final String name, final Object element )
{
if ( name == null )
{
return;
}
this.elements.put ( name, element );
}
public void addElement ( final Primitive primitive, final Object element )
{
if ( primitive == null )
{
return;
}
if ( primitive.getName () != null )
{
this.elements.put ( primitive.getName (), element );
}
this.primitives.put ( primitive, element );
}
public void removeElement ( final Primitive primitive )
{
if ( primitive == null )
{
return;
}
this.primitives.remove ( primitive );
this.elements.remove ( primitive.getName () );
}
public void unregisterItem ( final String name )
{
this.registrationManager.unregisterItem ( name );
}
public void registerItem ( final String name, final String itemId, final String connectionId, final boolean ignoreSummary, final boolean nullInvalid )
{
this.registrationManager.registerItem ( name, itemId, connectionId, ignoreSummary, nullInvalid );
}
/**
* Trigger the controller to update the data from the registration manager
* <p>
* This method can be called from any thread and must synchronized to the UI
* </p>
*/
public void triggerDataUpdate ()
{
try
{
Display.getDefault ().asyncExec ( new Runnable () {
@Override
public void run ()
{
handleDataUpdate ();
};
} );
}
catch ( final Exception e )
{
StatusManager.getManager ().handle ( StatusHelper.convertStatus ( Activator.PLUGIN_ID, e ) );
}
}
public Map<String, DataItemValue> getData ()
{
return convert ( this.registrationManager.getData () );
}
private Map<String, DataItemValue> convert ( final Map<String, DataValue> data )
{
final Map<String, DataItemValue> values = new LinkedHashMap<String, DataItemValue> ( data.size () );
for ( final Map.Entry<String, DataValue> entry : data.entrySet () )
{
if ( entry.getValue () == null )
{
continue;
}
values.put ( entry.getKey (), entry.getValue ().getValue () );
}
return values;
}
public SummaryInformation getSummaryInformation ()
{
return new SummaryInformation ( this.registrationManager.getData (), collectChildrenData () );
}
private Collection<SummaryInformation> collectChildrenData ()
{
final Collection<SummaryInformation> result = new LinkedList<SummaryInformation> ();
for ( final SymbolController controller : this.controllers )
{
result.add ( controller.getSummaryInformation () );
}
return result;
}
protected void handleDataUpdate ()
{
if ( this.registrationManager == null )
{
// dispose7
return;
}
final Map<String, DataValue> currentData = this.registrationManager.getData ();
if ( currentData == this.lastData )
{
return;
}
this.lastData = currentData;
runUpdate ();
// propagate update
if ( this.parentController != null )
{
this.parentController.runUpdate ();
}
}
private void runUpdate ()
{
try
{
this.onUpdate.execute ( this.scriptContext );
notifySummaryListeners ();
}
catch ( final Exception e )
{
StatusManager.getManager ().handle ( StatusHelper.convertStatus ( Activator.PLUGIN_ID, e ) );
}
}
protected void notifySummaryListeners ()
{
final SummaryInformation info = getSummaryInformation ();
for ( final SummaryListener listener : this.summaryListeners )
{
listener.summaryChanged ( info );
}
}
public void addSummaryListener ( final SummaryListener listener )
{
if ( this.summaryListeners.add ( listener ) )
{
listener.summaryChanged ( getSummaryInformation () );
}
}
public void removeSummaryListener ( final SummaryListener listener )
{
this.summaryListeners.remove ( listener );
}
public ScriptExecutor createScriptExecutor ( final String command ) throws ScriptException
{
if ( command == null || command.isEmpty () )
{
return null;
}
return new ScriptExecutor ( this.engine, command, this.classLoader );
}
public void execute ( final ScriptExecutor scriptExecutor, final Map<String, Object> scriptObjects )
{
if ( scriptExecutor == null )
{
return;
}
try
{
scriptExecutor.execute ( this.scriptContext, scriptObjects );
}
catch ( final Exception e )
{
StatusManager.getManager ().handle ( StatusHelper.convertStatus ( Activator.PLUGIN_ID, e ), StatusManager.LOG );
}
}
public void startWrite ( final String connectionId, final String itemId, final Variant value ) throws InterruptedException
{
this.registrationManager.startWrite ( connectionId, itemId, value );
}
public void startWriteAttributes ( final String connectionId, final String itemId, final Map<String, Variant> attributes ) throws InterruptedException
{
this.registrationManager.startWriteAttributes ( connectionId, itemId, attributes );
}
public void debugLog ( final String string )
{
final MessageConsoleStream stream = this.console.newMessageStream ();
final PrintWriter ps = new PrintWriter ( stream );
ps.println ( string );
try
{
ps.close ();
stream.close ();
}
catch ( final IOException e )
{
}
}
}