FELIX-2331 : webconsole event plugin should be able to send/post events.
https://issues.apache.org/jira/browse/FELIX-2331

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1172536 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/OptionalFeaturesHandler.java b/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/OptionalFeaturesHandler.java
index e951123..7559f90 100644
--- a/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/OptionalFeaturesHandler.java
+++ b/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/OptionalFeaturesHandler.java
@@ -21,6 +21,7 @@
 import java.util.Hashtable;
 
 import org.osgi.framework.*;
+import org.osgi.service.event.EventAdmin;
 
 /**
  * This class handles all optional stuff.
@@ -30,15 +31,16 @@
 public class OptionalFeaturesHandler
     implements ServiceListener
 {
-    private static final String EVENT_ADMIN_CLASS_NAME = "org.osgi.service.event.EventAdmin";
-    private static final String CONFIGURATION_ADMIN_CLASS_NAME = "org.osgi.service.cm.ConfigurationAdmin";
-    private static final String EVENT_HANDLER_CLASS_NAME = "org.osgi.service.event.EventHandler";
+    private static final String EVENT_ADMIN_CLASS_NAME = "org.osgi.service.event.EventAdmin"; //$NON-NLS-1$
+    private static final String CONFIGURATION_ADMIN_CLASS_NAME = "org.osgi.service.cm.ConfigurationAdmin"; //$NON-NLS-1$
+    private static final String EVENT_HANDLER_CLASS_NAME = "org.osgi.service.event.EventHandler"; //$NON-NLS-1$
 
     private static final String FILTER = "(|(" + Constants.OBJECTCLASS + "=" + EVENT_ADMIN_CLASS_NAME + ")"
                                         +"(" + Constants.OBJECTCLASS + "=" + CONFIGURATION_ADMIN_CLASS_NAME + "))";
 
     /** Event admin service id */
-    private Long eventAdminServiceId;
+    //private Long eventAdminServiceId;
+    private ServiceReference eventAdminServiceRef;
 
     /** Registration for the event handler. */
     private ServiceRegistration eventHandlerRegistration;
@@ -60,12 +62,10 @@
         this.plugin = plugin;
         this.bundleContext = context;
         // check if event admin is already available
-        this.eventAdminServiceId = null;
         final ServiceReference ref = this.bundleContext.getServiceReference(EVENT_ADMIN_CLASS_NAME);
         if ( ref != null )
         {
-            final Long id = (Long)ref.getProperty(Constants.SERVICE_ID);
-            bindEventAdmin(id);
+            bindEventAdmin(ref);
         }
 
         // check if config admin is already available
@@ -91,7 +91,7 @@
     public void destroy()
     {
         this.bundleContext.removeServiceListener(this);
-        this.unbindEventAdmin(this.eventAdminServiceId);
+        this.unbindEventAdmin(this.eventAdminServiceRef);
         this.unbindConfigAdmin(this.configAdminServiceId);
     }
 
@@ -100,14 +100,14 @@
      */
     public void serviceChanged(final ServiceEvent event)
     {
-        final String[] objectClasses =  (String[])event.getServiceReference().getProperty(Constants.OBJECTCLASS);
+        final ServiceReference ref = event.getServiceReference();
+        final String[] objectClasses =  (String[])ref.getProperty(Constants.OBJECTCLASS);
         if ( objectClasses != null)
         {
             for(int i=0; i<objectClasses.length; i++)
             {
                 if ( objectClasses[i].equals(EVENT_ADMIN_CLASS_NAME) )
                 {
-                    final Long id = (Long)event.getServiceReference().getProperty(Constants.SERVICE_ID);
                     if ( event.getType() == ServiceEvent.REGISTERED )
                     {
                         new Thread()
@@ -117,7 +117,7 @@
                                 try {
                                     Thread.sleep(500);
                                 } catch (InterruptedException ignore) {}
-                                bindEventAdmin(id);
+                                bindEventAdmin(ref);
                             }
                         }.start();
                     }
@@ -130,7 +130,7 @@
                                 try {
                                     Thread.sleep(500);
                                 } catch (InterruptedException ignore) {}
-                                unbindEventAdmin(id);
+                                unbindEventAdmin(ref);
                             }
                         }.start();
                     }
@@ -169,29 +169,30 @@
         }
     }
 
-    private synchronized void bindEventAdmin(final Long id)
+    synchronized void bindEventAdmin(ServiceReference ref)
     {
-        if ( this.eventAdminServiceId != null)
+        if ( this.eventAdminServiceRef != null)
         {
-            this.unbindEventAdmin(this.eventAdminServiceId);
+            this.unbindEventAdmin(this.eventAdminServiceRef);
         }
-        this.eventAdminServiceId = id;
+        this.eventAdminServiceRef = ref;
         final Dictionary props = new Hashtable();
         props.put( Constants.SERVICE_DESCRIPTION, "Event handler for the Apache Felix Web Console" );
         props.put( Constants.SERVICE_VENDOR, "The Apache Software Foundation" );
-        props.put( "event.topics", "*");
-        this.plugin.setEventAdminAvailable(true);
+        props.put( "event.topics", "*"); //$NON-NLS-1$ //$NON-NLS-2$
+        this.plugin.setEventAdmin((EventAdmin) bundleContext.getService(ref));
 
         this.eventHandlerRegistration = this.bundleContext.registerService(EVENT_HANDLER_CLASS_NAME,
                 new EventHandler(this.plugin.getCollector()), props);
     }
 
-    private synchronized void unbindEventAdmin(final Long id)
+    synchronized void unbindEventAdmin(ServiceReference ref)
     {
-        if ( this.eventAdminServiceId != null && this.eventAdminServiceId.equals(id) )
+        if ( this.eventAdminServiceRef != null && this.eventAdminServiceRef.equals(ref) )
         {
-            this.eventAdminServiceId = null;
-            this.plugin.setEventAdminAvailable(false);
+            bundleContext.ungetService(ref);
+            this.eventAdminServiceRef = null;
+            this.plugin.setEventAdmin(null);
             if ( this.eventHandlerRegistration != null )
             {
                 this.eventHandlerRegistration.unregister();
@@ -200,7 +201,7 @@
         }
     }
 
-    private synchronized void bindConfigAdmin(final Long id)
+    synchronized void bindConfigAdmin(final Long id)
     {
         if ( this.configAdminServiceId != null )
         {
@@ -211,7 +212,7 @@
         this.configListenerRegistration = ConfigurationListener.create(this.bundleContext, this.plugin);
     }
 
-    private synchronized void unbindConfigAdmin(final Long id)
+    synchronized void unbindConfigAdmin(final Long id)
     {
         if ( this.configAdminServiceId != null && this.configAdminServiceId.equals(id) )
         {
diff --git a/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/PluginServlet.java b/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/PluginServlet.java
index 1a4e141..8e8579c 100644
--- a/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/PluginServlet.java
+++ b/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/PluginServlet.java
@@ -28,30 +28,35 @@
 import javax.servlet.ServletException;
 import javax.servlet.http.*;
 
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+
 /**
  * The Event Plugin
  */
 public class PluginServlet extends HttpServlet
 {
-    private static final String ACTION_CLEAR = "clear";
+    
+    private static final String ACTION_POST = "post"; //$NON-NLS-1$
+    private static final String ACTION_SEND = "send"; //$NON-NLS-1$
+    private static final String ACTION_CLEAR = "clear"; //$NON-NLS-1$
 
-    private static final String PARAMETER_ACTION = "action";
+    private static final String PARAMETER_ACTION = "action"; //$NON-NLS-1$
 
     /** The event collector. */
     private final EventCollector collector;
 
-    /** Is the event admin available? */
-    private volatile boolean eventAdminAvailable = false;
-
     /** Is the config admin available? */
     private volatile boolean configAdminAvailable = false;
     
+    private EventAdmin eventAdmin;
+
     private final String TEMPLATE;
 
     public PluginServlet()
     {
         this.collector = new EventCollector(null);
-        TEMPLATE = readTemplateFile(getClass(), "/res/events.html");
+        TEMPLATE = readTemplateFile(getClass(), "/res/events.html"); //$NON-NLS-1$
     }
     
     private final String readTemplateFile(final Class clazz, final String templateFile)
@@ -68,7 +73,7 @@
                 {
                     baos.write(data, 0, len);
                 }
-                return baos.toString("UTF-8");
+                return baos.toString("UTF-8"); //$NON-NLS-1$
             }
             catch (IOException e)
             {
@@ -93,9 +98,17 @@
         // template file does not exist, return an empty string
         log("readTemplateFile: File '" + templateFile + "' not found through class "
             + clazz);
-        return "";
+        return ""; //$NON-NLS-1$
     }
+    
+    private static final Event newEvent(HttpServletRequest request)
+    {
+        String topic = request.getParameter("topic"); //$NON-NLS-1$
 
+        return new Event(topic, (Dictionary)PropertiesEditorSupport.convertProperties(request));
+    }
+    
+   
     /**
      * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
      */
@@ -103,14 +116,18 @@
     throws ServletException, IOException
     {
         final String action = req.getParameter( PARAMETER_ACTION );
-        // for now we only have the clear action
-        if ( ACTION_CLEAR.equals( action ) )
-        {
+        if ( ACTION_POST.equals(action) ) {
+            final Event event = newEvent(req);
+            eventAdmin.postEvent(event);
+        } else if (ACTION_SEND.equals(action)) {
+            final Event event = newEvent(req);
+            eventAdmin.sendEvent(event);
+        } else if ( ACTION_CLEAR.equals( action ) ) {
             this.collector.clear();
         }
         // we always send back the json data
-        resp.setContentType( "application/json" );
-        resp.setCharacterEncoding( "utf-8" );
+        resp.setContentType( "application/json" ); //$NON-NLS-1$
+        resp.setCharacterEncoding( "utf-8" ); //$NON-NLS-1$
 
         renderJSON( resp.getWriter() );
     }
@@ -135,7 +152,7 @@
             statusLine.append( d );
         }
         statusLine.append( ". (Event admin: " );
-        if ( !this.eventAdminAvailable )
+        if ( this.eventAdmin == null )
         {
             statusLine.append("un");
         }
@@ -172,7 +189,7 @@
 
         pw.write(']');
 
-        pw.write("}");
+        pw.write("}"); //$NON-NLS-1$
     }
 
 
@@ -181,10 +198,10 @@
     {
 
         final String info = request.getPathInfo();
-        if ( info.endsWith( ".json" ) )
+        if ( info.endsWith( ".json" ) ) //$NON-NLS-1$
         {
-            response.setContentType( "application/json" );
-            response.setCharacterEncoding( "UTF-8" );
+            response.setContentType( "application/json" ); //$NON-NLS-1$
+            response.setCharacterEncoding( "UTF-8" ); //$NON-NLS-1$
 
             PrintWriter pw = response.getWriter();
             this.renderJSON( pw );
@@ -208,7 +225,7 @@
 
     public URL getResource(String path)
     {
-        if ( path.startsWith("/events/res/ui/") )
+        if ( path.startsWith("/events/res/ui/") ) //$NON-NLS-1$
         {
             return this.getClass().getResource(path.substring(7));
         }
@@ -374,9 +391,9 @@
         return this.collector;
     }
 
-    public void setEventAdminAvailable(final boolean flag)
+    public void setEventAdmin(final EventAdmin eventAdmin)
     {
-        this.eventAdminAvailable = flag;
+        this.eventAdmin = eventAdmin;
     }
 
     public void setConfigAdminAvailable(final boolean flag)
diff --git a/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/PropertiesEditorSupport.java b/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/PropertiesEditorSupport.java
new file mode 100644
index 0000000..9170f8a
--- /dev/null
+++ b/webconsole-plugins/event/src/main/java/org/apache/felix/webconsole/plugins/event/internal/PropertiesEditorSupport.java
@@ -0,0 +1,95 @@
+/*

+ * Licensed to the Apache Software Foundation (ASF) under one or more

+ * contributor license agreements.  See the NOTICE file distributed with

+ * this work for additional information regarding copyright ownership.

+ * The ASF licenses this file to You 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

+ *

+ *      http://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.apache.felix.webconsole.plugins.event.internal;

+

+import java.util.Hashtable;

+

+import javax.servlet.http.HttpServletRequest;

+

+/**

+ * Java support for propeditor.js handling.

+ */

+public class PropertiesEditorSupport

+{

+

+    private PropertiesEditorSupport()

+    {

+        // prevent instantiation

+    }

+

+    /**

+     * Converts the properties from the request to a key-value hashtable.

+     * 

+     * @param request the request to process

+     * @return the converted properties

+     */

+    public static final Hashtable convertProperties(HttpServletRequest request)

+    {

+        String keys[] = request.getParameterValues("key"); //$NON-NLS-1$

+        String vals[] = request.getParameterValues("val"); //$NON-NLS-1$

+        String types[] = request.getParameterValues("type"); //$NON-NLS-1$

+

+        final Hashtable properties = new Hashtable();

+        synchronized (properties)

+        {

+            for (int i = 0; keys != null && i < keys.length; i++)

+            {

+                properties.put(keys[i], convert(vals[i], types[i]));

+            }

+        }

+

+        return properties;

+    }

+

+    private static final Object convert(String value, String type)

+    {

+        if ("byte".equals(type)) //$NON-NLS-1$

+        {

+            return Byte.valueOf(value);

+        }

+        else if ("int".equals(type)) //$NON-NLS-1$

+        {

+            return Integer.valueOf(value);

+        }

+        else if ("long".equals(type)) //$NON-NLS-1$

+        {

+            return Long.valueOf(value);

+        }

+        else if ("float".equals(type)) //$NON-NLS-1$

+        {

+            return Float.valueOf(value);

+        }

+        else if ("double".equals(type)) //$NON-NLS-1$

+        {

+            return Double.valueOf(value);

+        }

+        else if ("string".equals(type)) //$NON-NLS-1$

+        {

+            return value.toString();

+        }

+        else if ("char".equals(type)) //$NON-NLS-1$

+        {

+            return Character.valueOf(value.toString().charAt(0));

+        }

+        else

+        {

+            throw new IllegalArgumentException("Unsupported type!");

+        }

+        // TODO: hex, base64, sha1

+    }

+

+}

diff --git a/webconsole-plugins/event/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties b/webconsole-plugins/event/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties
index 7bc989e..72a0490 100644
--- a/webconsole-plugins/event/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties
+++ b/webconsole-plugins/event/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties
@@ -38,3 +38,10 @@
 properties=Атрибути

 

 plugin.events.title=Събития

+

+# send event

+sendEvent=Изпращане

+post=Прати

+send=Прати веднага

+close=Затваряне

+

diff --git a/webconsole-plugins/event/src/main/resources/OSGI-INF/l10n/bundle.properties b/webconsole-plugins/event/src/main/resources/OSGI-INF/l10n/bundle.properties
index 9402ce8..e9a94b0 100644
--- a/webconsole-plugins/event/src/main/resources/OSGI-INF/l10n/bundle.properties
+++ b/webconsole-plugins/event/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -37,4 +37,10 @@
 topic=Event Topic

 properties=Event Properties

 

-plugin.events.title=Events
\ No newline at end of file
+plugin.events.title=Events

+

+# send event

+sendEvent=Send/Post Event

+post=Post Event

+send=Send Event

+close=Close

diff --git a/webconsole-plugins/event/src/main/resources/res/events.html b/webconsole-plugins/event/src/main/resources/res/events.html
index 9fe35ac..4f7b700 100644
--- a/webconsole-plugins/event/src/main/resources/res/events.html
+++ b/webconsole-plugins/event/src/main/resources/res/events.html
@@ -1,8 +1,13 @@
-<script type="text/javascript" src="${pluginRoot}/res/ui/events.js"></script>

+<script type="text/javascript" src="${pluginRoot}/res/ui/addremove.js"></script>

+<script type="text/javascript" src="${pluginRoot}/res/ui/propeditor.js"></script>

+<script type="text/javascript" src="${pluginRoot}/res/ui/events.js"></script>

 <script type="text/javascript">

 var i18n = {

 	displayTimeline: '${displayTimeline}',

-	displayList    : '${displayList}'

+	displayList    : '${displayList}',

+	close          : '${close}',

+	send           : '${send}',

+	post           : '${post}'

 }

 </script>

 

@@ -13,6 +18,7 @@
 <div class="ui-widget-header ui-corner-top buttonGroup">

 	<button id="switch">${displayTimeline}</button>

 	<button id="clear">${clear}</button>

+	<button id="sendButton">${sendEvent}</button>

 	<button id="reload">${reload}</button>

 </div>

 

@@ -35,9 +41,27 @@
 </table>

 

 <div id="timeline" class="ui-helper-hidden">&nbsp;</div>

+

 <div id="timelineLegend" class="ui-helper-hidden">

 	<span class="event eventservice">Service Event</span>

 	<span class="event eventbundle">Bundle Event</span>

 	<span class="event eventconfig">Config Event</span>

 	<span class="event eventframework">Framework Event</span>

 </div>

+

+<div id="sendDialog" title="${sendEvent}" class="ui-helper-hidden">

+	<table>

+	<tbody>

+		<tr>

+			<th>${topic}:</th>

+			<td><input id="sendTopic"/></td>

+		</tr>

+		<tr>

+			<th>${properties}:</th>

+			<td>

+				<div id="sendProperties">&nbsp;</div>

+			</td>

+		</tr>

+	</tbody>

+	</table>

+</div>

diff --git a/webconsole-plugins/event/src/main/resources/res/ui/addremove.js b/webconsole-plugins/event/src/main/resources/res/ui/addremove.js
new file mode 100644
index 0000000..9ff4cef
--- /dev/null
+++ b/webconsole-plugins/event/src/main/resources/res/ui/addremove.js
@@ -0,0 +1,99 @@
+/*
+	Structure is:
+	<div class="my-element-container">
+		<div class="multiInput">
+			<div id="myElement" /> + -
+		</div>
+	</div>
+	
+	Options:
+	add : function(element) - called AFTER add
+	remove : function(element) - called BEFORE remove
+*/
+(function( $ ){
+
+	var methods = {
+		init : function(options) {
+			return this.each( function() {
+				// If options exist, lets merge them with our default settings
+				var settings = {
+					add    : false,
+					remove : false
+				};
+				if (options) settings = $.extend(settings, options);
+				
+				var _this = $(this);
+				var template = _init_template( _this );
+				_this.data('addremove_settings', settings);
+				_new_entry(template, _this);
+			})
+		},
+		reset : function() {
+			return this.each( function() {
+				var self = $(this);
+				self.find('div.addremove').not(':first').each( function() {
+					$(this).find('button.rem').click();
+				});
+			});
+		},
+		add : function(count) {
+			return this.each( function() {
+				var self = $(this);
+				var addfn = self.find('div.addremove:last button.add');
+				if (addfn.size()) {
+					var num = count ? count : 1;
+					for(var i=0; i<num; i++) addfn.click();
+				}
+			});
+		},
+		count : function() {
+			var self = $(this);
+			return $(this).find('div.addremove').size();
+		}
+	};
+
+	$.fn.addremove = function( method ) {
+		// Method calling logic
+		if ( methods[method] ) {
+		  return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
+		} else if ( typeof method === 'object' || ! method ) {
+		  return methods.init.apply( this, arguments );
+		} else {
+		  $.error( 'Method ' +  method + ' does not exist on jQuery.addremove' );
+		} 
+	};
+	
+	var _new_entry = function(template, container) {
+		var settings = container.data('addremove_settings');
+		var _entry = template.clone()
+			.find('button.add').click( function() {
+				_new_entry(template, container);
+				return false;
+			}).end()
+			.find('button.rem').click( function() {
+				if (container.addremove('count') > 1) {
+					if (typeof settings.remove == 'function') {
+						settings.remove(_entry);
+					}
+					_entry.remove();
+				}
+				return false;
+			}).end()
+			.appendTo(container);
+		if (typeof settings.add == 'function') settings.add(_entry);
+	}
+
+	var _init_template = function(entry) {
+		return _el('div', 'addremove')
+			.append(entry.children())
+			.append(_el('button', 'add').text('+'))
+			.append(_el('button', 'rem').text('-'));
+	}
+
+	var _el = function(el, clazz) {
+		var ret = $(document.createElement(el));
+		if (clazz) ret.addClass(clazz);
+		return ret;
+	}
+
+})( jQuery );
\ No newline at end of file
diff --git a/webconsole-plugins/event/src/main/resources/res/ui/events.css b/webconsole-plugins/event/src/main/resources/res/ui/events.css
index 2c25dfb..ea62a6b 100644
--- a/webconsole-plugins/event/src/main/resources/res/ui/events.css
+++ b/webconsole-plugins/event/src/main/resources/res/ui/events.css
@@ -24,4 +24,13 @@
 
 table.propTable, table.propTable tr, table.propTable td { border: none !important }
 td.propName { padding: 0 4px 0 0; text-align: right !important; text-decoration: underline }
-td.propVal   { padding: 0 0 0 4px }
\ No newline at end of file
+td.propVal   { padding: 0 0 0 4px }
+td.time { white-space: nowrap }
+
+/* send dialog styling */
+.addremove button { width: 16px; height: 16px; line-height: 10px; font-size: 10px; margin: 2px 2px }
+.addremove_inner { display: inline }
+.propeditor_entry select {	margin-left: 4px }
+#sendTopic { width: 100% }
+#sendDialog table { margin-left: auto; margin-right: auto }
+#sendDialog table th { text-align: right; font-weight: bold; padding-right: .5em }
diff --git a/webconsole-plugins/event/src/main/resources/res/ui/events.js b/webconsole-plugins/event/src/main/resources/res/ui/events.js
index 02cd849..2cd1abd 100644
--- a/webconsole-plugins/event/src/main/resources/res/ui/events.js
+++ b/webconsole-plugins/event/src/main/resources/res/ui/events.js
@@ -62,9 +62,9 @@
     }
 
 	$(tr( null, { id: 'entry' + dataEntry.id }, [
-		td( null, null, [ text( printDate(dataEntry.received) ) ] ),
-		td( null, null, [ text( dataEntry.topic ) ] ),
-		td( null, null, [ propE ] )
+		td( 'time', null, [ text( printDate(dataEntry.received) ) ] ),
+		td( 'topic', null, [ text( dataEntry.topic ) ] ),
+		td( 'detailes', null, [ propE ] )
 	])).appendTo(eventsBody);
 }
 
@@ -96,4 +96,60 @@
 	$('#reload').click(function() {
 		$.get(pluginRoot + '/data.json', null, renderData, 'json');
 	}).click();
+
+	function sendData(action) {
+		// check topic
+		var topic = sendTopic.val();
+		var topicOk = topic.match(/^[\w-]+(\/[\w-]+)*$/g) != null;
+		if (topicOk) {
+			sendTopic.removeClass('ui-state-error');
+		} else {
+			addTopic.removeClass('ui-state-error');
+		}
+		var data = sendProperties.propeditor('serialize');
+		if (topicOk && data != false) {
+			$.post(pluginRoot,
+				data.concat([
+					{name : 'action', value : action},
+					{name : 'topic', value : topic}
+				]),
+				renderData,
+				'json'
+			);
+			sendDialog.dialog("close");
+		}
+	}
+
+	/* send dialog code */
+	var sendButtons = {};
+	sendButtons[i18n.close] = function() {
+		$(this).dialog("close");
+	}
+	sendButtons[i18n.send] = function() {
+		sendData('send');
+	}
+	sendButtons[i18n.post] = function() {
+		sendData('post');
+	}
+	var sendDialog = $('#sendDialog').dialog({
+		autoOpen: false,
+		modal   : true,
+		width   : '40%',
+		buttons : sendButtons,
+		open    : function() {
+			sendTopic.val('');
+			sendProperties.propeditor('reset');
+		}
+	});
+	var sendTopic = $('#sendTopic');
+	var sendProperties = $('#sendProperties').propeditor({
+		add: function(el) {
+			el.find('select').addClass('dynhover');
+			initStaticWidgets(el);
+		}
+	});
+	$('#sendButton').click(function() {
+		sendDialog.dialog('open');
+	});
+
 });
diff --git a/webconsole-plugins/event/src/main/resources/res/ui/propeditor.js b/webconsole-plugins/event/src/main/resources/res/ui/propeditor.js
new file mode 100644
index 0000000..51f47d7
--- /dev/null
+++ b/webconsole-plugins/event/src/main/resources/res/ui/propeditor.js
@@ -0,0 +1,193 @@
+/*
+	Structure is:
+	<div class="propeditor">
+		<div>
+			<input class="key"/> = 
+			<input class="value"/>
+			<select>
+				<option>byte</option>
+				<option>int</option>
+				<option>long</option>
+				<option>float</option>
+				<option>double</option>
+				<option>string</option>
+				<option>char</option>
+				<option>hex</option>
+				<option>sha1</option>
+				<option>base64</option>
+			</select>
+		</div>
+	</div>
+	
+	Options:
+	validator : function(keyInputField, valInputField, type)
+*/
+(function( $ ){
+	var TYPES = ['byte', 'int', 'long', 'float', 'double', 'string', 'char', 'hex', 'base64', 'sha1'];
+
+	var methods = {
+		init : function(options) {
+			return this.each( function() {
+				// If options exist, lets merge them with our default settings
+				var settings = {
+					validator  : false,
+				};
+				if (options) settings = $.extend(settings, options);
+
+				var _this = $(this);
+				_this.data('propeditor_settings', settings);
+				_this.append(_entry());
+				_this.addremove(settings);
+			})
+		},
+		reset : function() {
+			return this.each( function() {
+				$(this).addremove('reset')
+					.find('.key').val('').end()
+					.find('.val').val('');
+			});
+		},
+		serialize : function() {
+			var self = $(this);
+			var validator = self.data('propeditor_settings').validator;
+			var result = new Array();
+			var ok = true;
+			var entries = $(this).find('div.addremove');
+			if (entries.size() == 1) {
+				var k = entries.find('.key').removeClass('ui-state-error').val();
+				var v = entries.find('.val').removeClass('ui-state-error').val();
+				if (k != '' || v != '') {
+					var data = _check_entry( entries, validator );
+					//if ( data == false ) ok = false; else result.push(data);
+					if ( data == false ) ok = false; else result = data;
+				}
+			} else {
+				entries.each(function() {
+					var data = _check_entry( $(this), validator );
+					//if ( data == false ) ok = false; else result.push(data);
+					if ( data == false ) ok = false; else result = result.concat(data);
+				});
+			}
+			return ok ? result : false;
+		},
+		setup : function(data, append) {
+			var self = $(this);
+			if (!append) self.propeditor('reset');
+			for (var i in data) {
+				self.addremove('add');
+				var d = data[i];
+				self.find('div.addremove:last')
+					.find('.key').val(d.key).end()
+					.find('.val').val(d.val).end()
+					.find('.typ').val(d.type);
+			}
+			if (!append) self.find('div.addremove:first').remove();
+		}
+	};
+
+	$.fn.propeditor = function( method ) {
+		// Method calling logic
+		if ( methods[method] ) {
+		  return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
+		} else if ( typeof method === 'object' || ! method ) {
+		  return methods.init.apply( this, arguments );
+		} else {
+		  $.error( 'Method ' +  method + ' does not exist on jQuery.tooltip' );
+		} 
+	};
+
+	var _el = function(el, clazz) {
+		var ret = $(document.createElement(el));
+		if (clazz) ret.addClass(clazz);
+		return ret;
+	}
+
+	var _entry = function() {
+		var sel = _el('select', 'typ');
+		for(var i in TYPES) {
+			sel.append( _el('option').text( TYPES[i] ) );
+		}
+		return _el('span', 'propeditor_entry')
+			.append( _el('input', 'key') )
+			.append( _el('span').text(' = '))
+			.append( _el('input', 'val') )
+			.append( sel );
+	}
+
+	var _check_entry = function(e, validator) {
+		var k = e.find('.key').removeClass('ui-state-error');
+		var v = e.find('.val').removeClass('ui-state-error');
+		var t = e.find('.typ').val();
+		var ok = _check_field(k);
+		ok = _check_field(v) && ok;
+		ok = ok && _defaultPropertyValidator(k, v, t);
+		if (ok && typeof validator == 'function') {
+			ok = validator(k, v, t);
+		}
+		if (ok) {
+			return [
+				{ 'name' : 'key', 'value' : k.val() },
+				{ 'name' : 'val', 'value' : v.val() },
+				{ 'name' : 'type', 'value' : t }
+			];
+			/*
+			return {
+				'key': k.val(),
+				'val': v.val(),
+				'type': t
+			}*/
+		}
+		return false;
+	}
+
+	var _check_field = function(f) {
+		if (!f.val()) {
+			f.addClass('ui-state-error');
+			return false;
+		}
+		return true;
+	}
+
+	var _range = function(field, isint, min, max) {
+		var v = false;
+		if (isint) {
+			var v = parseInt(field.val());
+			var xv = parseFloat(field.val());
+			if ( isNaN(v) || isNaN(xv) || xv != v) return false;  // field is actually double
+		} else { // double
+			v = parseFloat(field.val());
+			if (isNaN(v)) return false;
+		}
+
+		return v >= min && v <= max;
+	}
+	
+	// key == element, value == element, type == type string
+	var _defaultPropertyValidator = function(key, value, type) {
+		var v = value.val();
+		var ok = true;
+		switch(type) {
+			case 'byte':
+				ok = _range(value, true, -128, 127);
+				break;
+			case 'int':
+				ok = _range(value, true, -2147483648, 2147483647);
+				break;
+			case 'long':
+				ok = _range(value, true, -9223372036854775808, 9223372036854775807);
+				break;
+			case 'float':
+				ok = _range(value, false, 1.4E-45, 3.4E38);
+				break;
+			case 'double':
+				ok = _range(value, false, 4.9E-324, 1.7E308);
+				break;
+			case 'char':
+				ok = v.length == 1;
+				break;
+		}
+		if (!ok) value.addClass('ui-state-error');
+		return ok;
+	}
+
+})( jQuery );
\ No newline at end of file