/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.ui.classic.internal.servlet;
import java.io.IOException;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.eclipse.emf.common.util.EList;
import org.eclipse.smarthome.core.items.GenericItem;
import org.eclipse.smarthome.core.items.Item;
import org.eclipse.smarthome.core.items.ItemNotFoundException;
import org.eclipse.smarthome.core.items.StateChangeListener;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.model.sitemap.Frame;
import org.eclipse.smarthome.model.sitemap.LinkableWidget;
import org.eclipse.smarthome.model.sitemap.Sitemap;
import org.eclipse.smarthome.model.sitemap.SitemapProvider;
import org.eclipse.smarthome.model.sitemap.Widget;
import org.eclipse.smarthome.ui.classic.internal.WebAppConfig;
import org.eclipse.smarthome.ui.classic.internal.render.PageRenderer;
import org.eclipse.smarthome.ui.classic.render.RenderException;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is the main servlet for the Classic UI.
* It serves the Html code based on the sitemap model.
*
* @author Kai Kreuzer - Initial contribution and API
*
*/
public class WebAppServlet extends BaseServlet {
private final Logger logger = LoggerFactory.getLogger(WebAppServlet.class);
/**
* timeout for polling requests in milliseconds; if no state changes during this time,
* an empty response is returned.
*/
private static final long TIMEOUT_IN_MS = 30000L;
/** the name of the servlet to be used in the URL */
public static final String SERVLET_NAME = "app";
private PageRenderer renderer;
protected Set<SitemapProvider> sitemapProviders = new CopyOnWriteArraySet<>();
private WebAppConfig config = new WebAppConfig();
public void addSitemapProvider(SitemapProvider sitemapProvider) {
this.sitemapProviders.add(sitemapProvider);
}
public void removeSitemapProvider(SitemapProvider sitemapProvider) {
this.sitemapProviders.remove(sitemapProvider);
}
public void setPageRenderer(PageRenderer renderer) {
renderer.setConfig(config);
this.renderer = renderer;
}
protected void activate(Map<String, Object> configProps) {
config.applyConfig(configProps);
try {
Hashtable<String, String> props = new Hashtable<String, String>();
httpService.registerServlet(WEBAPP_ALIAS + "/" + SERVLET_NAME, this, props, createHttpContext());
httpService.registerResources(WEBAPP_ALIAS, "web", null);
logger.info("Started Classic UI at " + WEBAPP_ALIAS + "/" + SERVLET_NAME);
} catch (NamespaceException e) {
logger.error("Error during servlet startup", e);
} catch (ServletException e) {
logger.error("Error during servlet startup", e);
}
}
protected void modified(Map<String, Object> configProps) {
config.applyConfig(configProps);
}
protected void deactivate() {
httpService.unregister(WEBAPP_ALIAS + "/" + SERVLET_NAME);
httpService.unregister(WEBAPP_ALIAS);
logger.info("Stopped Classic UI");
}
/**
* {@inheritDoc}
*/
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
logger.debug("Servlet request received!");
// read request parameters
String sitemapName = req.getParameter("sitemap");
String widgetId = req.getParameter("w");
boolean async = "true".equalsIgnoreCase(req.getParameter("__async"));
boolean poll = "true".equalsIgnoreCase(req.getParameter("poll"));
// if there are no parameters, display the "default" sitemap
if (sitemapName == null) {
sitemapName = config.getDefaultSitemap();
}
StringBuilder result = new StringBuilder();
Sitemap sitemap = null;
for (SitemapProvider sitemapProvider : sitemapProviders) {
sitemap = sitemapProvider.getSitemap(sitemapName);
if (sitemap != null) {
break;
}
}
try {
if (sitemap == null) {
throw new RenderException("Sitemap '" + sitemapName + "' could not be found");
}
logger.debug("reading sitemap {}", sitemap.getName());
if (widgetId == null || widgetId.isEmpty() || widgetId.equals("Home")) {
// we are at the homepage, so we render the children of the sitemap root node
String label = sitemap.getLabel() != null ? sitemap.getLabel() : sitemapName;
EList<Widget> children = sitemap.getChildren();
if (poll && waitForChanges(children) == false) {
// we have reached the timeout, so we do not return any content as nothing has changed
res.getWriter().append(getTimeoutResponse()).close();
return;
}
result.append(renderer.processPage("Home", sitemapName, label, sitemap.getChildren(), async));
} else if (!widgetId.equals("Colorpicker")) {
// we are on some subpage, so we have to render the children of the widget that has been selected
Widget w = renderer.getItemUIRegistry().getWidget(sitemap, widgetId);
if (w != null) {
String label = renderer.getItemUIRegistry().getLabel(w);
if (label == null) {
label = "undefined";
}
if (!(w instanceof LinkableWidget)) {
throw new RenderException("Widget '" + w + "' can not have any content");
}
EList<Widget> children = renderer.getItemUIRegistry().getChildren((LinkableWidget) w);
if (poll && waitForChanges(children) == false) {
// we have reached the timeout, so we do not return any content as nothing has changed
res.getWriter().append(getTimeoutResponse()).close();
return;
}
result.append(renderer.processPage(renderer.getItemUIRegistry().getWidgetId(w), sitemapName, label,
children, async));
}
}
} catch (RenderException e) {
throw new ServletException(e.getMessage(), e);
}
if (async) {
res.setContentType("application/xml;charset=UTF-8");
} else {
res.setContentType("text/html;charset=UTF-8");
}
res.getWriter().append(result);
res.getWriter().close();
}
/**
* Defines the response to return on a polling timeout.
*
* @return the response of the servlet on a polling timeout
*/
private String getTimeoutResponse() {
return "<root><part><destination mode=\"replace\" zone=\"timeout\" create=\"false\"/><data/></part></root>";
}
/**
* This method only returns when a change has occurred to any item on the page to display
*
* @param widgets the widgets of the page to observe
*/
private boolean waitForChanges(EList<Widget> widgets) {
long startTime = (new Date()).getTime();
boolean timeout = false;
BlockingStateChangeListener listener = new BlockingStateChangeListener();
// let's get all items for these widgets
Set<GenericItem> items = getAllItems(widgets);
for (GenericItem item : items) {
item.addStateChangeListener(listener);
}
while (!listener.hasChangeOccurred() && !timeout) {
timeout = (new Date()).getTime() - startTime > TIMEOUT_IN_MS;
try {
Thread.sleep(300);
} catch (InterruptedException e) {
timeout = true;
break;
}
}
for (GenericItem item : items) {
item.removeStateChangeListener(listener);
}
return !timeout;
}
/**
* Collects all items that are represented by a given list of widgets
*
* @param widgets the widget list to get the items for
* @return all items that are represented by the list of widgets
*/
private Set<GenericItem> getAllItems(EList<Widget> widgets) {
Set<GenericItem> items = new HashSet<GenericItem>();
if (itemRegistry != null) {
for (Widget widget : widgets) {
String itemName = widget.getItem();
if (itemName != null) {
try {
Item item = itemRegistry.getItem(itemName);
if (item instanceof GenericItem) {
final GenericItem gItem = (GenericItem) item;
items.add(gItem);
}
} catch (ItemNotFoundException e) {
// ignore
}
} else {
if (widget instanceof Frame) {
items.addAll(getAllItems(((Frame) widget).getChildren()));
}
}
}
}
return items;
}
/**
* This is a state change listener, which is merely used to determine, if a state
* change has occurred on one of a list of items.
*
* @author Kai Kreuzer - Initial contribution and API
*
*/
private static class BlockingStateChangeListener implements StateChangeListener {
private boolean changed = false;
/**
* {@inheritDoc}
*/
@Override
public void stateChanged(Item item, State oldState, State newState) {
changed = true;
}
/**
* determines, whether a state change has occurred since its creation
*
* @return true, if a state has changed
*/
public boolean hasChangeOccurred() {
return changed;
}
/**
* {@inheritDoc}
*/
@Override
public void stateUpdated(Item item, State state) {
// ignore if the state did not change
}
}
}