Resolved FELIX-2121
Initial import of ProSyst donation of UPnP plugin for the web console

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@925979 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/Activator.java b/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/Activator.java
new file mode 100644
index 0000000..059ad4f
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/Activator.java
@@ -0,0 +1,109 @@
+/*

+ * 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.upnp.internal;

+

+import org.apache.felix.webconsole.SimpleWebConsolePlugin;

+import org.osgi.framework.BundleActivator;

+import org.osgi.framework.BundleContext;

+import org.osgi.framework.ServiceReference;

+import org.osgi.service.upnp.UPnPDevice;

+import org.osgi.util.tracker.ServiceTracker;

+import org.osgi.util.tracker.ServiceTrackerCustomizer;

+

+/**

+ * Activator is the main starting class.

+ */

+public class Activator implements BundleActivator, ServiceTrackerCustomizer

+{

+

+    private ServiceTracker tracker;

+    private BundleContext context;

+

+    private SimpleWebConsolePlugin plugin;

+

+    /**

+     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)

+     */

+    public final void start(BundleContext context) throws Exception

+    {

+        this.context = context;

+        this.tracker = new ServiceTracker(context, UPnPDevice.class.getName(), this);

+        this.tracker.open();

+    }

+

+    /**

+     * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)

+     */

+    public final void stop(BundleContext context) throws Exception

+    {

+        if (tracker != null)

+        {

+            tracker.close();

+            tracker = null;

+        }

+    }

+

+    // - begin tracker

+    /**

+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#modifiedService(org.osgi.framework.ServiceReference,

+     *      java.lang.Object)

+     */

+    public final void modifiedService(ServiceReference reference, Object service)

+    {/* unused */

+    }

+

+    /**

+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#addingService(org.osgi.framework.ServiceReference)

+     */

+    public final Object addingService(ServiceReference reference)

+    {

+        SimpleWebConsolePlugin plugin = this.plugin;

+        if (plugin == null)

+        {

+            this.plugin = plugin = new WebConsolePlugin(tracker).register(context);

+        }

+        // delegate event

+        ControlServlet controller = ((WebConsolePlugin) plugin).controller;

+        if (controller != null)

+            controller.addingService(reference);

+

+        return context.getService(reference);

+    }

+

+    /**

+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#removedService(org.osgi.framework.ServiceReference,

+     *      java.lang.Object)

+     */

+    public final void removedService(ServiceReference reference, Object service)

+    {

+        SimpleWebConsolePlugin plugin = this.plugin;

+

+        if (plugin != null)

+        {

+            ControlServlet controller = ((WebConsolePlugin) plugin).controller;

+            if (controller != null)

+                controller.removedService(reference, service);

+        }

+

+        if (tracker.getTrackingCount() == 0 && plugin != null)

+        {

+            plugin.unregister();

+            this.plugin = null;

+        }

+

+    }

+}

diff --git a/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/ControlServlet.java b/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/ControlServlet.java
new file mode 100644
index 0000000..1e626a6
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/ControlServlet.java
@@ -0,0 +1,492 @@
+/*

+ * 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.upnp.internal;

+

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.OutputStream;

+import java.lang.reflect.Constructor;

+import java.text.SimpleDateFormat;

+import java.util.Date;

+import java.util.Dictionary;

+import java.util.Enumeration;

+import java.util.HashMap;

+import java.util.Hashtable;

+import java.util.Iterator;

+

+import javax.servlet.ServletException;

+import javax.servlet.http.HttpServlet;

+import javax.servlet.http.HttpServletRequest;

+import javax.servlet.http.HttpServletResponse;

+

+import org.json.JSONException;

+import org.json.JSONObject;

+import org.osgi.framework.BundleContext;

+import org.osgi.framework.ServiceReference;

+import org.osgi.service.upnp.UPnPAction;

+import org.osgi.service.upnp.UPnPDevice;

+import org.osgi.service.upnp.UPnPIcon;

+import org.osgi.service.upnp.UPnPLocalStateVariable;

+import org.osgi.service.upnp.UPnPService;

+import org.osgi.service.upnp.UPnPStateVariable;

+import org.osgi.util.tracker.ServiceTracker;

+import org.osgi.util.tracker.ServiceTrackerCustomizer;

+

+/**

+ * This class handles requests from the Web Interface. It is separated from

+ * the WebConsolePlugin just to improve readability. This servlet actually

+ * is not registered in HTTP service.

+ */

+public class ControlServlet extends HttpServlet implements ServiceTrackerCustomizer

+{

+

+    private static final SimpleDateFormat DATA_FORMAT = new SimpleDateFormat(

+        "EEE, d MMM yyyy HH:mm:ss Z");

+

+    final HashMap icons = new HashMap(10);

+    final HashMap sessions = new HashMap(10);

+

+    private ServiceTracker tracker;

+    private final BundleContext bc;

+

+    private static final long LAST_MODIFIED = System.currentTimeMillis();

+

+    /**

+     * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,

+     *      javax.servlet.http.HttpServletResponse)

+     */

+    protected void doGet(HttpServletRequest request, HttpServletResponse response)

+        throws ServletException, IOException

+    {

+

+        String udn = request.getParameter("icon");

+

+        if (udn != null)

+        {

+            UPnPIcon icon = (UPnPIcon) icons.get(udn);

+            if (icon == null)

+            {

+                response.sendError(HttpServletResponse.SC_NOT_FOUND);

+            }

+            else

+            {

+                if (request.getDateHeader("If-Modified-Since") > 0)

+                {

+                    // if it is already in cache - don't bother to go further

+                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);

+                }

+                else

+                {

+                    // enable caching

+                    response.setDateHeader("Last-Modified", LAST_MODIFIED);

+

+                    String mime = icon.getMimeType();

+                    if (mime != null)

+                        response.setContentType(mime);

+                    OutputStream out = response.getOutputStream();

+

+                    int size = icon.getSize();

+                    if (size > 0)

+                        response.setContentLength(size);

+

+                    InputStream in = icon.getInputStream();

+                    // can't use buffer, because it's might block if reading byte[]

+                    int read;

+                    while (-1 != (read = in.read()))

+                        out.write(read);

+                }

+            }

+        }

+    }

+

+    /**

+     * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest,

+     *      javax.servlet.http.HttpServletResponse)

+     */

+    protected void doPost(HttpServletRequest request, HttpServletResponse response)

+        throws ServletException, IOException

+    {

+        try

+        {

+            JSONObject json = new JSONObject();

+

+            String method = request.getParameter("action");

+

+            if ("listDevices".equals(method))

+            {

+                getSession(request).unsubscribe();

+

+                ServiceReference[] refs = tracker.getServiceReferences();

+                // add root devices only

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

+                {

+                    if (refs[i] != null

+                        && refs[i].getProperty(UPnPDevice.PARENT_UDN) == null)

+                    {

+                        json.append("devices", deviceTreeToJSON(refs[i]));

+                    }

+                }

+            }

+            else if ("serviceDetails".equals(method))

+            {

+                UPnPService service = requireService(request);

+                SessionObject session = getSession(request)//

+                .subscribe(require("udn", request), service.getId());

+

+                json = serviceToJSON(service, session);

+            }

+            else if ("invokeAction".equals(method))

+            {

+                UPnPService service = requireService(request);

+                UPnPAction action = service.getAction(require("actionID", request));

+

+                json = invoke(

+                    action, //

+                    request.getParameterValues("names"),

+                    request.getParameterValues("vals"));

+            }

+            else

+            {

+                throw new ServletException("Invalid action: " + method);

+            }

+

+            response.setContentType("application/json");

+            response.setCharacterEncoding("UTF-8");

+            response.getWriter().print(json.toString(2));

+        }

+        catch (ServletException e)

+        {

+            throw e;

+        }

+        catch (Exception e)

+        {

+            e.printStackTrace();

+            throw new ServletException(e.toString());

+        }

+    }

+

+    private final SessionObject getSession(HttpServletRequest request)

+    {

+        String sessionID = request.getSession().getId();

+        SessionObject ret = (SessionObject) sessions.get(sessionID);

+        if (ret == null)

+        {

+            ret = new SessionObject(bc, sessionID, sessions);

+            request.getSession().setAttribute("___upnp.session.object", ret);

+        }

+        return ret;

+    }

+

+    private static final String require(String name, HttpServletRequest request)

+        throws ServletException

+    {

+        String value = request.getParameter(name);

+        if (value == null)

+            throw new ServletException("missing parameter: " + name);

+        return value;

+    }

+

+    private final UPnPService requireService(HttpServletRequest request)

+        throws ServletException

+    {

+        String deviceUdn = require("udn", request);

+        String serviceUrn = require("urn", request);

+

+        UPnPDevice device = getDevice(deviceUdn);

+        return getService(device, serviceUrn);

+    }

+

+    private final JSONObject deviceTreeToJSON(ServiceReference ref) throws JSONException

+    {

+        UPnPDevice device = (UPnPDevice) tracker.getService(ref);

+        Object[] refs = tracker.getServiceReferences();

+

+        Object parentUdn = ref.getProperty(UPnPDevice.UDN);

+        JSONObject json = deviceToJSON(ref, device);

+

+        // add child devices

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

+        {

+            ref = (ServiceReference) refs[i];

+

+            Object parent = ref.getProperty(UPnPDevice.PARENT_UDN);

+            Object currentUDN = ref.getProperty(UPnPDevice.UDN);

+            if (parent == null)

+            { // no parent

+                continue;

+            }

+            else if (currentUDN != null && currentUDN.equals(parent))

+            { // self ?

+                continue;

+            }

+            else if (parentUdn.equals(parent))

+            {

+                device = (UPnPDevice) tracker.getService(ref);

+                json.append("children", deviceTreeToJSON(ref));

+            }

+        }

+        return json;

+    }

+

+    private static final JSONObject deviceToJSON(ServiceReference ref, UPnPDevice device)

+        throws JSONException

+    {

+        JSONObject json = new JSONObject();

+        json.put("icon", device.getIcons(null) != null);

+

+        // add properties

+        String[] props = ref.getPropertyKeys();

+        JSONObject _props = new JSONObject();

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

+        {

+            _props.put(props[i], ref.getProperty(props[i]));

+        }

+        json.put("props", _props);

+

+        UPnPService[] services = device.getServices();

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

+        {

+            json.append("services", services[i].getType());

+        }

+

+        return json;

+    }

+

+    private static final JSONObject serviceToJSON(UPnPService service,

+        SessionObject session) throws JSONException

+    {

+        JSONObject json = new JSONObject();

+

+        // add service properties

+        json.put("type", service.getType());

+        json.put("id", service.getId());

+

+        // add state variables

+        UPnPStateVariable[] vars = service.getStateVariables();

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

+        {

+            Object value = null;

+            if (vars[i] instanceof UPnPLocalStateVariable)

+            {

+                value = ((UPnPLocalStateVariable) vars[i]).getCurrentValue();

+            }

+

+            if (value == null)

+                value = session.getValue(vars[i].getName());

+            if (value == null)

+                value = "---";

+

+            json.append("variables", new JSONObject() //

+            .put("name", vars[i].getName()) //

+            .put("value", value) //

+            .put("defalt", vars[i].getDefaultValue()) //

+            .put("min", vars[i].getMinimum()) //

+            .put("max", vars[i].getMaximum()) //

+            .put("step", vars[i].getStep()) //

+            .put("allowed", vars[i].getAllowedValues()) //

+            .put("sendsEvents", vars[i].sendsEvents()) //

+            );

+        }

+

+        // add actions

+        UPnPAction[] actions = service.getActions();

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

+        {

+            json.append("actions", actionToJSON(actions[i]));

+        }

+

+        return json;

+    }

+

+    private static final JSONObject actionToJSON(UPnPAction action) throws JSONException

+    {

+        JSONObject json = new JSONObject();

+        json.put("name", action.getName());

+        String[] names = action.getInputArgumentNames();

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

+        {

+            UPnPStateVariable variable = action.getStateVariable(names[i]);

+            json.append("inVars", new JSONObject()//

+            .put("name", names[i])//

+            .put("type", variable.getUPnPDataType()));

+        }

+

+        return json;

+

+    }

+

+    private static final JSONObject invoke(UPnPAction action, String[] names,

+        String[] vals) throws Exception

+    {

+        JSONObject json = new JSONObject();

+

+        // check input arguments

+        Hashtable inputArgs = null;

+        if (names != null && vals != null && names.length > 0

+            && names.length == vals.length)

+        {

+            inputArgs = new Hashtable(names.length);

+            for (int i = 0; i < names.length; i++)

+            {

+                UPnPStateVariable var = action.getStateVariable(names[i]);

+                Class javaType = var.getJavaDataType();

+                Constructor constructor = javaType.getConstructor(new Class[] { String.class });

+                Object argObj = constructor.newInstance(new Object[] { vals[i] });

+

+                inputArgs.put(names[i], argObj);

+            }

+        }

+

+        // invoke

+        Dictionary out = action.invoke(inputArgs);

+

+        // prepare output arguments

+        if (out != null && out.size() > 0)

+        {

+            for (Enumeration e = out.keys(); e.hasMoreElements();)

+            {

+                String key = (String) e.nextElement();

+                UPnPStateVariable var = action.getStateVariable(key);

+

+                Object value = out.get(key);

+                if (value instanceof Date)

+                {

+                    synchronized (DATA_FORMAT)

+                    {

+                        value = DATA_FORMAT.format((Date) value);

+                    }

+                }

+                else if (value instanceof byte[])

+                {

+                    value = hex((byte[]) value);

+                }

+

+                json.append("output", new JSONObject() //

+                .put("name", key)//

+                .put("type", var.getUPnPDataType()) //

+                .put("value", value));

+            }

+        }

+        return json;

+    }

+

+    private static final String hex(byte[] data)

+    {

+        if (data == null)

+            return "null";

+        StringBuffer sb = new StringBuffer(data.length * 3);

+        synchronized (sb)

+        {

+            for (int i = 0; i < data.length; i++)

+            {

+                sb.append(Integer.toHexString(data[i] & 0xff)).append('-');

+            }

+            sb.deleteCharAt(sb.length() - 1);

+        }

+        return sb.toString();

+    }

+

+    private final UPnPDevice getDevice(String udn)

+    {

+        ServiceReference[] refs = tracker.getServiceReferences();

+        String _udn;

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

+        {

+            _udn = (String) refs[i].getProperty(UPnPDevice.UDN);

+            if (_udn != null && _udn.equals(udn))

+            {

+                return (UPnPDevice) tracker.getService(refs[i]);

+            }

+        }

+

+        throw new IllegalArgumentException("Device '" + udn + "' not found!");

+    }

+

+    private final UPnPService getService(UPnPDevice device, String urn)

+    {

+        UPnPService[] services = device.getServices();

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

+        {

+            if (services[i].getType().equals(urn))

+            {

+                return services[i];

+            }

+        }

+

+        throw new IllegalArgumentException("Service '" + urn + "' not found!");

+    }

+

+    /**

+     * Creates new XML-RPC handler.

+     * 

+     * @param bc the bundle context

+     * @param iconServlet the icon servlet.

+     */

+    ControlServlet(BundleContext bc, ServiceTracker tracker)

+    {

+        this.bc = bc;

+        this.tracker = tracker;

+    }

+

+    /**

+     * Cancels the scheduled timers

+     */

+    void close()

+    {

+        icons.clear();

+        for (Iterator i = sessions.values().iterator(); i.hasNext();)

+        {

+            ((SessionObject) i.next()).unsubscribe();

+        }

+        sessions.clear();

+    }

+

+    /* ---------- BEGIN SERVICE TRACKER */

+    /**

+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#modifiedService(org.osgi.framework.ServiceReference,

+     *      java.lang.Object)

+     */

+    public final void modifiedService(ServiceReference ref, Object serv)

+    {/* unused */

+    }

+

+    /**

+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#removedService(org.osgi.framework.ServiceReference,

+     *      java.lang.Object)

+     */

+    public final void removedService(ServiceReference ref, Object serv)

+    {

+        icons.remove(ref.getProperty(UPnPDevice.UDN));

+    }

+

+    /**

+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#addingService(org.osgi.framework.ServiceReference)

+     */

+    public final Object addingService(ServiceReference ref)

+    {

+        UPnPDevice device = (UPnPDevice) bc.getService(ref);

+

+        UPnPIcon[] _icons = device.getIcons(null);

+        if (_icons != null && _icons.length > 0)

+        {

+            icons.put(ref.getProperty(UPnPDevice.UDN), _icons[0]);

+        }

+

+        return device;

+    }

+

+}

diff --git a/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/SessionObject.java b/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/SessionObject.java
new file mode 100644
index 0000000..c64cce0
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/SessionObject.java
@@ -0,0 +1,153 @@
+/*

+ * 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.upnp.internal;

+

+import java.util.Dictionary;

+import java.util.Enumeration;

+import java.util.HashMap;

+import java.util.Hashtable;

+import java.util.Map;

+

+import javax.servlet.http.HttpSessionBindingEvent;

+import javax.servlet.http.HttpSessionBindingListener;

+

+import org.osgi.framework.BundleContext;

+import org.osgi.framework.InvalidSyntaxException;

+import org.osgi.framework.ServiceRegistration;

+import org.osgi.service.upnp.UPnPDevice;

+import org.osgi.service.upnp.UPnPEventListener;

+import org.osgi.service.upnp.UPnPService;

+

+/**

+ * The reason for having this SessionObject is the strange event delivery in UPnP. It's not possible

+ * read a state variable value, but if register a listener, you will get notified when the value has

+ * changed.

+ */

+final class SessionObject implements HttpSessionBindingListener, UPnPEventListener

+{

+

+    static final String LISTENER_CLASS = UPnPEventListener.class.getName();

+

+    private final Map vars = new HashMap();

+    private final String sessionID;

+    private final Map store;

+    private String udn, urn;

+

+    private final BundleContext bc;

+    private final Hashtable regProps = new Hashtable(3);

+    private ServiceRegistration reg;

+

+    SessionObject(BundleContext bc, String sessionID, Map store)

+    {

+        this.bc = bc;

+        this.sessionID = sessionID;

+        this.store = store;

+    }

+

+    /**

+     * @see javax.servlet.http.HttpSessionBindingListener#valueBound(javax.servlet.http.HttpSessionBindingEvent)

+     */

+    public void valueBound(HttpSessionBindingEvent event)

+    {

+        store.put(sessionID, this);

+    }

+

+    /**

+     * @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(javax.servlet.http.HttpSessionBindingEvent)

+     */

+    public final void valueUnbound(HttpSessionBindingEvent event)

+    {

+        unsubscribe();

+        store.remove(sessionID); // remove from list of sessions

+    }

+

+    /**

+     * @see org.osgi.service.upnp.UPnPEventListener#notifyUPnPEvent(java.lang.String,

+     *      java.lang.String, java.util.Dictionary)

+     */

+    public final void notifyUPnPEvent(String deviceId, String serviceId, Dictionary events)

+    {

+        if (sameDevice(deviceId, serviceId))

+        {

+            for (Enumeration e = events.keys(); e.hasMoreElements();)

+            {

+                Object key = e.nextElement();

+                vars.put(key, events.get(key));

+            }

+        }

+    }

+

+    private final boolean sameDevice(String udn, String urn)

+    {

+        String _udn = this.udn;

+        String _urn = this.urn;

+        if (_udn == null || _urn == null)

+            return false; // not subscribed

+        return _udn.equals(udn) && _urn.equals(urn);

+    }

+

+    final synchronized SessionObject subscribe(String udn, String urn)

+    {

+        if (!sameDevice(udn, urn))

+        {

+            unsubscribe();

+            this.udn = udn;

+            this.urn = urn;

+

+            try

+            {

+                regProps.put(UPnPEventListener.UPNP_FILTER, bc.createFilter(//

+                "(&(" + UPnPDevice.UDN + '=' + udn + ")(" + //

+                    UPnPService.ID + '=' + urn + "))"));

+            }

+            catch (InvalidSyntaxException e)

+            { /* will not happen */

+            }

+            reg = bc.registerService(LISTENER_CLASS, this, regProps);

+        }

+        return this;

+    }

+

+    final synchronized SessionObject unsubscribe()

+    {

+        this.udn = this.urn = null;

+        vars.clear();

+        if (reg != null)

+        {

+            reg.unregister();

+            reg = null;

+        }

+        return this;

+    }

+

+    final Object getValue(String name)

+    {

+        return vars.get(name);

+    }

+

+    /**

+     * @see java.lang.Object#toString()

+     */

+    public final String toString()

+    {

+        StringBuffer buffer = new StringBuffer();

+        buffer.append("SessionObject [sessionID=").append(sessionID).append(", udn=").append(

+            udn).append(", urn=").append(urn).append(", vars=").append(vars).append("]");

+        return buffer.toString();

+    }

+

+}

diff --git a/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/WebConsolePlugin.java b/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/WebConsolePlugin.java
new file mode 100644
index 0000000..ce29eaa
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/java/org/apache/felix/webconsole/plugins/upnp/internal/WebConsolePlugin.java
@@ -0,0 +1,127 @@
+/*

+ * 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.upnp.internal;

+

+import java.io.IOException;

+

+import javax.servlet.ServletException;

+import javax.servlet.http.HttpServletRequest;

+import javax.servlet.http.HttpServletResponse;

+

+import org.apache.felix.webconsole.SimpleWebConsolePlugin;

+import org.osgi.framework.BundleContext;

+import org.osgi.util.tracker.ServiceTracker;

+

+/**

+ * Provides Web Console interface to UPnP Devices.

+ */

+public class WebConsolePlugin extends SimpleWebConsolePlugin

+{

+

+    private static final String LABEL = "upnp";

+    private static final String TITLE = "UPnP";

+    private static final String CSS[] = { "/" + LABEL + "/res/upnp.css", //

+            "/" + LABEL + "/res/jquery-treeview-1.4/jquery.treeview.css", //

+    };

+

+    private final ServiceTracker tracker;

+    ControlServlet controller;

+

+    //templates

+    private final String TEMPLATE;

+

+    /**

+     * Creates new plugin

+     * 

+     * @param tracker the UPnP Device tracker

+     */

+    public WebConsolePlugin(ServiceTracker tracker)

+    {

+        super(LABEL, TITLE, CSS);

+        this.tracker = tracker;

+

+        // load templates

+        TEMPLATE = readTemplateFile("/res/upnp.html");

+    }

+

+    /**

+     * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#activate(org.osgi.framework.BundleContext)

+     */

+    public void activate(BundleContext bundleContext)

+    {

+        super.activate(bundleContext);

+        controller = new ControlServlet(bundleContext, tracker);

+    }

+

+    /**

+     * @see org.apache.felix.webconsole.SimpleWebConsolePlugin#deactivate()

+     */

+    public void deactivate()

+    {

+        if (controller != null)

+        {

+            controller.close();

+            controller = null;

+        }

+        super.deactivate();

+    }

+

+    /**

+     * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest,

+     *      javax.servlet.http.HttpServletResponse)

+     */

+    protected void renderContent(HttpServletRequest req, HttpServletResponse res)

+        throws ServletException, IOException

+    {

+        res.getWriter().print(TEMPLATE);

+    }

+

+    /**

+     * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest,

+     *      javax.servlet.http.HttpServletResponse)

+     */

+    protected void doPost(HttpServletRequest request, HttpServletResponse response)

+        throws ServletException, IOException

+    {

+        if (request.getParameter("action") != null)

+        {

+            controller.doPost(request, response);

+        }

+        else

+        {

+            super.doPost(request, response);

+        }

+    }

+

+    /**

+     * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#doGet(javax.servlet.http.HttpServletRequest,

+     *      javax.servlet.http.HttpServletResponse)

+     */

+    protected void doGet(HttpServletRequest request, HttpServletResponse response)

+        throws ServletException, IOException

+    {

+        if (request.getParameter("icon") != null)

+        {

+            controller.doGet(request, response);

+        }

+        else

+        {

+            super.doGet(request, response);

+        }

+    }

+

+}

diff --git a/webconsole-plugins/upnp/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties b/webconsole-plugins/upnp/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties
new file mode 100644
index 0000000..c6503e7
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/native2ascii/OSGI-INF/l10n/bundle_bg.properties
@@ -0,0 +1,26 @@
+# Convert to normal properties using: native2ascii.exe -encoding utf-8 bundle_bg.properties.utf8 bundle_bg.properties

+status.ok=UPnP работи. Вижте по-долу за наличните устройсва.

+btn.search=Търсене на устройства

+btn.reloadVars=Презареждане на променливите

+tree.searching=Търсене на устройства...

+tree.loading.title=Зареждане...

+device.title=Информация за устройството

+prop.name=Атрибут

+prop.value=Стойност

+service.title=Информация за услугата

+service.id=ID:

+service.type=Тип:

+vars.title=Променливи

+vars.name=Име

+vars.value=Стойност

+vars.events=Изпраща нотификации

+actions.title=Действия

+actions.invoke=Изпълнение

+args.name=Име на аргумента

+args.type=UPnP тип

+args.value=Стойност

+dialog.title.ok=Успех!

+no.actions=Няма асоциирани действия

+no.params.out=Няма изходни параметри

+actions.select=Действие:

+actions.invoke.title=Изпълнение на избраното действие

diff --git a/webconsole-plugins/upnp/src/main/resources/OSGI-INF/l10n/bundle.properties b/webconsole-plugins/upnp/src/main/resources/OSGI-INF/l10n/bundle.properties
new file mode 100644
index 0000000..6971558
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -0,0 +1,25 @@
+status.ok=UPnP Service Available.

+btn.search=Search for UPnP Devices

+btn.reloadVars=Reload State Variables

+tree.searching=Searching for UPnP Devices ...

+tree.loading.title=Loading

+device.title=Device Information

+prop.name=Property Name

+prop.value=Value

+service.title=Service Information

+service.id=Service ID:

+service.type=Service Type:

+vars.title=State Variables

+vars.name=Name

+vars.value=Value

+vars.events=Sends Events

+actions.title=Actions

+actions.invoke=Invoke

+args.name=Argument Name

+args.type=UPnP Data Type

+args.value=Value

+dialog.title.ok=Success

+no.actions=No actions available.

+no.params.out=No output parameters.

+actions.select=Select Action:

+actions.invoke.title=Invoke the selected action

diff --git a/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/minus.gif b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/minus.gif
new file mode 100644
index 0000000..47fb7b7
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/minus.gif
Binary files differ
diff --git a/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/plus.gif b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/plus.gif
new file mode 100644
index 0000000..6906621
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/plus.gif
Binary files differ
diff --git a/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/treeview-default-line.gif b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/treeview-default-line.gif
new file mode 100644
index 0000000..37114d3
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/treeview-default-line.gif
Binary files differ
diff --git a/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/treeview-default.gif b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/treeview-default.gif
new file mode 100644
index 0000000..a12ac52
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/images/treeview-default.gif
Binary files differ
diff --git a/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/jquery.treeview.css b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/jquery.treeview.css
new file mode 100644
index 0000000..5ced4d4
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/jquery.treeview.css
@@ -0,0 +1,56 @@
+.treeview, .treeview ul { 

+	padding: 0;

+	margin: 0;

+	list-style: none;

+}

+

+.treeview ul {

+	background-color: white;

+	margin-top: 4px;

+}

+

+.treeview .hitarea {

+	background: url(images/treeview-default.gif) -64px -25px no-repeat;

+	height: 16px;

+	width: 16px;

+	margin-left: -16px;

+	float: left;

+	cursor: pointer;

+}

+/* fix for IE6 */

+* html .hitarea {

+	display: inline;

+	float:none;

+}

+

+.treeview li { 

+	margin: 0;

+	padding: 3px 0pt 3px 16px;

+}

+

+.treeview a.selected {

+	background-color: #eee;

+}

+

+#treecontrol { margin: 1em 0; display: none; }

+

+.treeview .hover { color: red; cursor: pointer; }

+

+.treeview li { background: url(images/treeview-default-line.gif) 0 0 no-repeat; }

+.treeview li.collapsable, .treeview li.expandable { background-position: 0 -176px; }

+

+.treeview .expandable-hitarea { background-position: -80px -3px; }

+

+.treeview li.last { background-position: 0 -1766px }

+.treeview li.lastCollapsable, .treeview li.lastExpandable { background-image: url(images/treeview-default.gif); }  

+.treeview li.lastCollapsable { background-position: 0 -111px }

+.treeview li.lastExpandable { background-position: -32px -67px }

+

+.treeview div.lastCollapsable-hitarea, .treeview div.lastExpandable-hitarea { background-position: 0; }

+

+

+.filetree li { padding: 3px 0 2px 16px; }

+.filetree span.folder, .filetree span.file { padding: 1px 0 1px 16px; display: block; }

+.filetree span.folder { background: url(images/folder.gif) 0 0 no-repeat; }

+.filetree li.expandable span.folder { background: url(images/folder-closed.gif) 0 0 no-repeat; }

+.filetree span.file { background: url(images/file.gif) 0 0 no-repeat; }

diff --git a/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/jquery.treeview.pack.js b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/jquery.treeview.pack.js
new file mode 100644
index 0000000..eddac49
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/jquery-treeview-1.4/jquery.treeview.pack.js
@@ -0,0 +1,16 @@
+/*
+ * Treeview 1.4 - jQuery plugin to hide and show branches of a tree
+ * 
+ * http://bassistance.de/jquery-plugins/jquery-plugin-treeview/
+ * http://docs.jquery.com/Plugins/Treeview
+ *
+ * Copyright (c) 2007 Jörn Zaefferer
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id: jquery.treeview.js 4684 2008-02-07 19:08:06Z joern.zaefferer $
+ *
+ */
+eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}(';(4($){$.1l($.F,{E:4(b,c){l a=3.n(\'.\'+b);3.n(\'.\'+c).o(c).m(b);a.o(b).m(c);8 3},s:4(a,b){8 3.n(\'.\'+a).o(a).m(b).P()},1n:4(a){a=a||"1j";8 3.1j(4(){$(3).m(a)},4(){$(3).o(a)})},1h:4(b,a){b?3.1g({1e:"p"},b,a):3.x(4(){T(3)[T(3).1a(":U")?"H":"D"]();7(a)a.A(3,O)})},12:4(b,a){7(b){3.1g({1e:"D"},b,a)}1L{3.D();7(a)3.x(a)}},11:4(a){7(!a.1k){3.n(":r-1H:G(9)").m(k.r);3.n((a.1F?"":"."+k.X)+":G(."+k.W+")").6(">9").D()}8 3.n(":y(>9)")},S:4(b,c){3.n(":y(>9):G(:y(>a))").6(">1z").C(4(a){c.A($(3).19())}).w($("a",3)).1n();7(!b.1k){3.n(":y(>9:U)").m(k.q).s(k.r,k.t);3.G(":y(>9:U)").m(k.u).s(k.r,k.v);3.1r("<J 14=\\""+k.5+"\\"/>").6("J."+k.5).x(4(){l a="";$.x($(3).B().1o("14").13(" "),4(){a+=3+"-5 "});$(3).m(a)})}3.6("J."+k.5).C(c)},z:4(g){g=$.1l({N:"z"},g);7(g.w){8 3.1K("w",[g.w])}7(g.p){l d=g.p;g.p=4(){8 d.A($(3).B()[0],O)}}4 1m(b,c){4 L(a){8 4(){K.A($("J."+k.5,b).n(4(){8 a?$(3).B("."+a).1i:1I}));8 1G}}$("a:10(0)",c).C(L(k.u));$("a:10(1)",c).C(L(k.q));$("a:10(2)",c).C(L())}4 K(){$(3).B().6(">.5").E(k.Z,k.Y).E(k.I,k.M).P().E(k.u,k.q).E(k.v,k.t).6(">9").1h(g.1f,g.p);7(g.1E){$(3).B().1D().6(">.5").s(k.Z,k.Y).s(k.I,k.M).P().s(k.u,k.q).s(k.v,k.t).6(">9").12(g.1f,g.p)}}4 1d(){4 1C(a){8 a?1:0}l b=[];j.x(4(i,e){b[i]=$(e).1a(":y(>9:1B)")?1:0});$.V(g.N,b.1A(""))}4 1c(){l b=$.V(g.N);7(b){l a=b.13("");j.x(4(i,e){$(e).6(">9")[1y(a[i])?"H":"D"]()})}}3.m("z");l j=3.6("Q").11(g);1x(g.1w){18"V":l h=g.p;g.p=4(){1d();7(h){h.A(3,O)}};1c();17;18"1b":l f=3.6("a").n(4(){8 3.16.15()==1b.16.15()});7(f.1i){f.m("1v").1u("9, Q").w(f.19()).H()}17}j.S(g,K);7(g.R){1m(3,g.R);$(g.R).H()}8 3.1t("w",4(a,b){$(b).1s().o(k.r).o(k.v).o(k.t).6(">.5").o(k.I).o(k.M);$(b).6("Q").1q().11(g).S(g,K)})}});l k=$.F.z.1J={W:"W",X:"X",q:"q",Y:"q-5",M:"t-5",u:"u",Z:"u-5",I:"v-5",v:"v",t:"t",r:"r",5:"5"};$.F.1p=$.F.z})(T);',62,110,'|||this|function|hitarea|find|if|return|ul||||||||||||var|addClass|filter|removeClass|toggle|expandable|last|replaceClass|lastExpandable|collapsable|lastCollapsable|add|each|has|treeview|apply|parent|click|hide|swapClass|fn|not|show|lastCollapsableHitarea|div|toggler|handler|lastExpandableHitarea|cookieId|arguments|end|li|control|applyClasses|jQuery|hidden|cookie|open|closed|expandableHitarea|collapsableHitarea|eq|prepareBranches|heightHide|split|class|toLowerCase|href|break|case|next|is|location|deserialize|serialize|height|animated|animate|heightToggle|length|hover|prerendered|extend|treeController|hoverClass|attr|Treeview|andSelf|prepend|prev|bind|parents|selected|persist|switch|parseInt|span|join|visible|binary|siblings|unique|collapsed|false|child|true|classes|trigger|else'.split('|'),0,{}))
\ No newline at end of file
diff --git a/webconsole-plugins/upnp/src/main/resources/res/loading.gif b/webconsole-plugins/upnp/src/main/resources/res/loading.gif
new file mode 100644
index 0000000..a4242e4
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/loading.gif
Binary files differ
diff --git a/webconsole-plugins/upnp/src/main/resources/res/upnp.css b/webconsole-plugins/upnp/src/main/resources/res/upnp.css
new file mode 100644
index 0000000..2e5d5ce
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/upnp.css
@@ -0,0 +1,39 @@
+/*

+ * 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.

+ */

+#detailsBox { padding-left: .5em }

+#dialog .nicetable { font-size: .5em }

+#detailsBox h3 {

+	font-weight: bold;

+	font-size: 1.5em;

+	padding-top: 1em;

+}

+#treeBox , #treeCont {

+	width: 300px !important;

+	height: 100%;

+	min-height: 300px;

+	overflow: auto;

+}

+#browser { margin: 1em; height: 100% }

+#browser li { cursor: pointer }

+#browser span.ui-icon { display: inline-block }

+img.icon { /* force image size */

+	border: none;

+	width: 16px;

+	height: 16px;

+	margin: 0;

+	padding: 0;

+}

diff --git a/webconsole-plugins/upnp/src/main/resources/res/upnp.html b/webconsole-plugins/upnp/src/main/resources/res/upnp.html
new file mode 100644
index 0000000..67e1d5f
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/upnp.html
@@ -0,0 +1,121 @@
+<script type="text/javascript" src="${pluginRoot}/res/jquery-treeview-1.4/jquery.treeview.pack.js"></script>

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

+<script type="text/javascript">

+// <![CDATA[

+// i18n

+var i18n = {

+	dl_title_ok  : '${dialog.title.ok}',

+	args_name    : '${args.name}',

+	args_type    : '${args.type}',

+	args_value   : '${args.value}',

+	no_actions   : '${no.actions}',

+	no_params_out: '${no.params.out}'

+}

+// ]]>

+</script>

+

+<p class="statline">${status.ok}</p>

+

+<form method="post" enctype="multipart/form-data" action="" onsubmit="return false">

+	<div class="ui-widget-header ui-corner-top buttonGroup">

+		<button id="reloadDevices">${btn.search}</button>

+		<button id="reloadVars" class="ui-state-disabled">${btn.reloadVars}</button>

+	</div>

+</form>

+

+<table id="plugin_table" style="width: 100%">

+	<tr>

+		<td id="treeCont" class="ui-widget-content ui-corner-bottom">

+			<div id="searching"> <!-- search for devices -->

+				${tree.searching}

+				<img src="${pluginRoot}/res/loading.gif" alt="${tree.loading.title}" />

+			</div>

+			<div id="treeBox"> <!-- here comes the tree -->

+				<ul id="browser">

+					<li>dummy</li>

+				</ul>

+			</div>

+		</td>

+		<td id="detailsBox">

+			<!-- opened when device is selected -->

+			<div id="deviceData" class="ui-helper-hidden">

+				<h3>${device.title}</h3>

+				<table id="deviceTable" class="nicetable">

+					<thead>

+						<tr>

+							<th>${prop.name}</th>

+							<th>${prop.value}</th>

+						</tr>

+					</thead>

+					<tbody>

+						<tr><td colspan="2">&nbsp;</td></tr> <!-- dynamic contents -->

+					</tbody>

+				</table>

+			</div>

+			<div id="serviceData" class="ui-helper-hidden">

+				<h3>${service.title}</h3>

+				<table class="nicetable">

+					<thead>

+						<tr>

+							<th>${prop.name}</th>

+							<th>${prop.value}</th>

+						</tr>

+					</thead>

+					<tbody>

+						<tr>

+							<td class="ui-priority-primary">${service.id}</td>

+							<td id="serviceDataInfoID">&nbsp;</td><!-- dynamic contents -->

+						</tr> 

+						<tr>

+							<td class="ui-priority-primary">${service.type}</td>

+							<td id="serviceDataInfoType">&nbsp;</td><!-- dynamic contents -->

+						</tr> 

+					</tbody>

+				</table>

+

+				<h3>${vars.title}</h3>

+				<table id="serviceDataVars" class="tablesorter nicetable">

+					<thead>

+						<tr>

+							<th class="col_Name">${vars.name}</th>

+							<th class="col_Value">${vars.value}</th>

+							<th class="col_SendEvents">${vars.events}</th>

+						</tr>

+					</thead>

+					<tbody><!-- dynamic contents -->

+						<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr> 

+					</tbody>

+				</table>

+

+				<div id="actionsContainer">

+					<h3>${actions.title}</h3>

+					<div class="ui-widget-header ui-corner-top buttonGroup">

+						${actions.select}

+						<select name="c">

+							<option value="---">---</option>

+						</select>

+						<button title="${actions.invoke.title}"><span class="ui-icon ui-icon-play">&nbsp;</span></button>

+					</div>

+					<table class="nicetable">

+						<thead>

+							<tr>

+								<th>${args.name}</th>

+								<th>${args.type}</th>

+								<th>${args.value}</th>

+							</tr>

+						</thead>

+						<tbody>

+							<tr><!-- template -->

+								<td>&nbsp;</td>

+								<td>&nbsp;</td>

+								<td><input value="" /></td>

+							</tr>

+						</tbody>

+					</table>

+				</div>

+			</div> <!-- serviceData -->

+			&nbsp;

+		</td>

+	</tr>

+</table>

+

diff --git a/webconsole-plugins/upnp/src/main/resources/res/upnp.js b/webconsole-plugins/upnp/src/main/resources/res/upnp.js
new file mode 100644
index 0000000..8899f7d
--- /dev/null
+++ b/webconsole-plugins/upnp/src/main/resources/res/upnp.js
@@ -0,0 +1,326 @@
+/*

+ * 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.

+ */

+// device selection

+var deviceData = false;

+var deviceTableBody = false;

+

+// service selection

+var serviceData = false;

+var serviceDataVars = false;

+var serviceDataInfoID = false;

+var serviceDataInfoType = false;

+

+// actions

+var actionsContainer = false;

+var actionsSelect    = false;

+var actionsInvoke    = false;

+var actionsTable     = false;

+var actionsTableBody = false;

+var actionsTableRow  = false;

+

+// tree browser, buttons, error dialog

+var browser = false;

+var searching = false;

+var reloadVars = false;

+

+/* BEGIN HELPERS */

+

+/* helper functions for tree node */

+function _id(dn) { return dn.replace(/[:-\\.\\$]/g,'_') }

+/* creates a node in the device tree */

+function treeNode(id, name, icon, span) {

+	var li = createElement('li', null, { 'id' : _id(id) }, [

+		createElement('span', null, null, [

+			icon ? 

+				createElement('img', 'icon', { 'src' : icon }) :

+				createElement('span', 'ui-icon ui-icon-' + span) ,

+			text(name)

+		])

+	]);

+	return $(li);

+}

+/* creates a service node in the devices tree, and associates the click action */

+function servNode(udn, urn) {

+	return treeNode(udn+urn, urn, null, 'extlink').click(function() {

+		if (selectServiceTime) {

+			clearTimeout(selectServiceTime);

+			selectServiceTime = false;

+		}

+		$.post(pluginRoot, { 

+			'action': 'serviceDetails',

+			'udn' : udn,

+			'urn' : urn

+		}, function(data) {

+			renderService(udn, urn, data)

+		}, 'json');

+		return false;

+	});

+}

+/* a helper function to format device properties values - specially 

+ * converts arrays and strings, if the last are links */

+function _val(val) {

+	var ret = '';

+	if ($.isArray(val)) {

+		for (i in val) ret += _val(val[i]) + '<br/>';

+	} else {

+		ret = (typeof val == 'string' && val.indexOf('http://') != -1) ?

+			'<a target="blank" href="' + val + '">' + val + '</a>' : val;

+	}

+	return ret;

+}

+

+

+/* BEGIN UI-ELEMENTS CREATION */

+

+/* add element to the tree, just creates the node */

+function addDevice(device) {

+	var udn  = device.props['UPnP.device.UDN'];

+	var name = device.props['UPnP.device.friendlyName'];

+	var icon = null;

+	if (device.icon) icon = pluginRoot + '?icon=' + udn;

+

+	var node = treeNode(udn, name, icon, 'lightbulb').click(function() {

+		renderDevice(device);

+	});

+

+	var ul, hasChildren;

+

+	// services

+	hasChildren = false;

+	ul = $(createElement('ul', 'ui-helper-clearfix'));

+	for(var i in device.services) {

+		hasChildren = true;

+		ul.append( servNode(udn, device.services[i]) );

+	}

+	if (hasChildren) node.append(ul);

+

+	// child devices

+	hasChildren = false;

+	ul = $(createElement('ul'));

+	for(var i in device.children) {

+		hasChildren = true;

+		addDevice(device.children[i]);

+	}

+	if (hasChildren) node.append(ul);

+

+	return node;

+}

+/* fills in the list of state variables */

+function renderVars(data) {

+	serviceDataVars.empty();

+	for(i in data.variables) {

+		var _var = data.variables[i];

+		var _tr = tr(null, null, [

+			td(null, null, [ text(_var.name) ]),

+			td(null, null, [ text(_var.value) ]),

+			td(null, null, [ text(_var.sendsEvents) ])

+		]);

+		serviceDataVars.append(_tr);

+	}

+	initStaticWidgets();

+}

+

+/* BEGIN ACTION HANDLERS */

+

+var selectedDevice = false; // the LI element of the selected device, reset on load

+function renderDevice(device) {

+	// generate content

+	var table = '';

+	for(var key in device.props) {

+		table += '<tr><td class="ui-priority-primary">' + key + '</td><td>' + _val(device.props[key]) + '</td></tr>';

+	}

+

+	// update the UI

+	deviceTableBody.html(table);

+	reloadVars.addClass('ui-state-disabled');

+	deviceData.removeClass('ui-helper-hidden');

+	serviceData.addClass('ui-helper-hidden')

+	

+	// reset selected items

+	if (selectedDevice) selectedDevice.css('font-weight', 'normal');

+	selectedDevice = $('#' + _id(device.props['UPnP.device.UDN']) + ' span').css('font-weight', 'bold');

+}

+

+var selectedUdn = false;

+var selectedUrn = false;

+var selectServiceTime = false;

+function renderService(udn, urn, data) {

+	// save selection

+	selectedUdn = udn;

+	selectedUrn = urn;

+

+	// append service info

+	serviceDataInfoID.text(data.id);

+	serviceDataInfoType.text(data.type);

+

+	// append state variables

+	renderVars(data);

+

+	// append actions

+	if (data.actions) {

+		var html = '';

+		var x = data.actions;

+		for (var a in x) html += '<option value="' + a + '">' + x[a].name + '</option>';

+		actionsSelect.html(html).unbind('change').change(function() {

+			var index = $(this).val();

+			actionSelected(udn, urn, x[index]);

+		}).trigger('change');

+		actionsContainer.removeClass('ui-helper-hidden');

+	} else {

+		actionsContainer.addClass('ui-helper-hidden');

+	}

+

+	// update UI

+	deviceData.addClass('ui-helper-hidden');

+	serviceData.removeClass('ui-helper-hidden');

+	reloadVars.removeClass('ui-state-disabled');

+	initStaticWidgets();

+

+	// refresh once - to get updates asynchronously

+	selectServiceTime = setTimeout('reloadVars.click()', 3000);

+}

+

+function actionSelected(udn, urn, action) {

+	// add input arguments

+	if (action.inVars) {

+		actionsTableBody.empty();

+		for (var i in action.inVars) {

+			var _arg = action.inVars[i];

+			var _tr = actionsTableRow.clone().appendTo(actionsTableBody);

+			_tr.find('td:eq(0)').text(_arg.name);

+			_tr.find('td:eq(1)').text(_arg.type);

+			var _el = _tr.find('input').attr('id', 'arg'+i);

+			_arg['element'] = _el;

+		}

+		actionsTable.removeClass('ui-helper-hidden');

+	} else {

+		actionsTable.addClass('ui-helper-hidden');

+	}

+	

+	actionsInvoke.unbind('click').click(function() {

+		invokeAction(udn, urn, action);

+	});

+

+	initStaticWidgets(actionsTableBody);

+}

+

+function invokeAction(udn, urn, action) {

+	// prepare arguments

+	var names = new Array();

+	var vals = new Array();

+	for (var i in action.inVars) {

+		var x = action.inVars[i];

+		names.push(x['name']);

+		vals.push(x['element'].val());

+	}

+	// invoke action

+	$.post(pluginRoot, { 

+		'udn' : udn,

+		'urn' : urn,

+		'action': 'invokeAction',

+		'actionID' : action.name,

+		'names' : names,

+		'vals'  : vals

+	}, function(data) {

+		var html = i18n.no_params_out;

+		if (data.output) {

+			html = '<table class="nicetable"><tr><th>'+i18n.args_name+'</th><th>'+i18n.args_type+'</th><th>' + i18n.args_value + '</th></tr>';

+			for(var i in data.output) {

+				var arg = data.output[i];

+				html += '<tr><td>' + arg['name'] + '</td><td>' + arg['type'] + '</td><td>' + arg['value'] + '</td></tr>';

+			}

+			html += '</table>';

+		}

+		Xalert(html, i18n.dl_title_ok);

+	}, 'json');

+}

+

+function listDevices() {

+	browser.empty().addClass('ui-helper-hidden');

+	searching.removeClass('ui-helper-hidden');

+	

+	$.post(pluginRoot, { 'action': 'listDevices' }, function(data) {

+		if (data && data.devices) {

+			$.each(data.devices, function(index) {

+				var html = addDevice(this);

+				browser.treeview( { add: html.appendTo(browser) } );

+			});

+		} else {

+			browser.append('','No devices available', '');

+		}

+

+		// update selected items

+		selectedDevice = false;

+		selectedUdn = false;

+		selectedUrn = false;

+	

+		// update IU elements

+		browser.removeClass('ui-helper-hidden');

+		searching.addClass('ui-helper-hidden');

+		deviceData.addClass('ui-helper-hidden');

+		serviceData.addClass('ui-helper-hidden');

+	}, 'json');

+

+	return false;

+}

+

+

+

+$(document).ready( function() {

+	// init elements of style

+	searching          = $('#searching');

+	deviceData         = $('#deviceData');

+	deviceTableBody    = $('#deviceTable tbody');

+

+	// services

+	serviceData        = $('#serviceData');

+	serviceDataInfoID  = $('#serviceDataInfoID');

+	serviceDataInfoType= $('#serviceDataInfoType');

+	serviceDataVars    = $('#serviceDataVars tbody');

+

+	// actions

+	actionsContainer   = $('#actionsContainer');

+	actionsSelect      = actionsContainer.find('select');

+	actionsInvoke      = actionsContainer.find('button');

+	actionsTable       = actionsContainer.find('table');

+	actionsTableBody   = actionsTable.find('tbody');

+	actionsTableRow    = actionsTableBody.find('tr').clone();

+	actionsTableBody.empty();

+

+	// init navigation tree

+	browser = $('#browser').treeview({

+		animated: 'fast',

+		collapsed: true,

+		unique: true

+	});

+	

+	// reload button

+	reloadVars = $('#reloadVars').click(function() {

+		if (selectedUdn && selectedUrn) {

+			$.post(pluginRoot, { 

+				'action': 'serviceDetails',

+				'udn' : selectedUdn,

+				'urn' : selectedUrn

+			}, renderVars, 'json');

+		}

+	})

+

+	$('#reloadDevices').click(listDevices);

+

+	listDevices();

+});

+