Java tutorial
/* * OpenRemote, the Home of the Digital Home. * Copyright 2008-2015, OpenRemote Inc. * * See the contributors.txt file in the distribution for a * full listing of individual contributors. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.openremote.controller.service; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.commons.io.FileUtils; import org.jdom.Attribute; import org.jdom.Element; import org.openremote.controller.Constants; import org.openremote.controller.ControllerConfiguration; import org.openremote.controller.OpenRemoteRuntime; import org.openremote.controller.SecurityPasswordStorage; import org.openremote.controller.deployer.ModelBuilder; import org.openremote.controller.deployer.Version20ModelBuilder; import org.openremote.controller.deployer.Version30ModelBuilder; import org.openremote.controller.exception.ConfigurationException; import org.openremote.controller.exception.ConnectionException; import org.openremote.controller.exception.ControllerDefinitionNotFoundException; import org.openremote.controller.exception.InitializationException; import org.openremote.controller.exception.OpenRemoteException; import org.openremote.controller.exception.XMLParsingException; import org.openremote.controller.model.XMLMapping; import org.openremote.controller.model.sensor.Sensor; import org.openremote.controller.statuscache.StatusCache; import org.openremote.controller.utils.HttpUtils; import org.openremote.controller.utils.Logger; import org.openremote.devicediscovery.domain.DiscoveredDeviceDTO; import org.openremote.rest.GenericResourceResultWithErrorMessage; import org.openremote.security.KeyManager; import org.openremote.security.PasswordManager; import org.openremote.useraccount.domain.ControllerDTO; import org.restlet.Client; import org.restlet.Context; import org.restlet.data.ChallengeScheme; import org.restlet.data.Protocol; import org.restlet.ext.json.JsonRepresentation; import org.restlet.representation.Representation; import org.restlet.resource.ClientResource; import org.springframework.security.providers.encoding.Md5PasswordEncoder; import flexjson.JSONDeserializer; import flexjson.JSONSerializer; /** * Deployer service centralizes access to the controller's runtime state information. It maintains * the controller object model (declared in the XML documents controller deploys), and also * acts as a mediator for some other key services in the controller. <p> * * Mainly the tasks relate to objects and services that maintain some state in-memory of * the controller, and where access to such objects or services needs to be shared across * multiple threads (rather than created per thread or invocation). Deployer manages the lifecycle * of such stateful objects and services to ensure proper state transitions when new controller * definitions (from the XML model) are loaded. <p> * * Main parts of this implementation relate to managing the XML to Java object mapping -- * transferring the information from the XML document instance that describe controller * behavior into a runtime object model -- and managing the lifecycle of services through * restarts and reloading the controller descriptions. In addition, this deployer provides * access to the object model instances it generates (such as references to the sensor * implementations). * * * @see #softRestart * @see #getSensor * * @author <a href="mailto:juha@openremote.org">Juha Lindfors</a> * @author <a href="mailto:eric@openremote.org">Eric Bariaux</a> */ public class Deployer { /* * TODO: * * - ORCJAVA-123 (http://jira.openremote.org/browse/ORCJAVA-123) : introduce an immutable * sensor interface for plugins to use. * - ORCJAVA-173 (http://jira.openremote.org/browse/ORCJAVA-123) : expose some of the admin * related functions through a secured REST interface -- deployFromZip, deployFromOnline, * softRestart * - ORCJAVA-174 (http://jira.openremote.org/browse/ORCJAVA-174) : start sensors * asynchronously * - ORCJAVA-179 (http://jira.openremote.org/browse/ORCJAVA-179) : guard against multiple * invocations of startController() * - ORCJAVA-180 (http://jira.openremote.org/browse/ORCJAVA-180) : add shutdown hook * - ORCJAVA-188 (http://jira.openremote.org/browse/ORCJAVA-188) : introduce lifecycle * interface */ // Class Members -------------------------------------------------------------------------------- /** * Common log category for startup logging, with a specific sub-category for this deployer * implementation. */ private final static Logger log = Logger.getLogger(Constants.DEPLOYER_LOG_CATEGORY); // Private Fields ------------------------------------------------------------------------------- /** * Reference to status cache instance that does the actual lifecycle management of sensors * (and receives the event updates). This implementation delegates these tasks to it. */ private StatusCache deviceStateCache; /** * User defined controller configuration variables. * * TODO : See ORCJAVA-183 */ private ControllerConfiguration controllerConfig; /** * Configured model builder implementations for this deployer, per schema version. */ private Map<ModelBuilder.SchemaVersion, ModelBuilder> builders; private Map<Integer, Element> controllerXMLElementCache = new HashMap<Integer, Element>(); /** * Cache parsed XML elements to avoid triggering XML parser/XPath multiple times during * runtime execution. * * This is a temporary performance fix to an issue described in ORCJAVA-190 -- once the * relevant overhaul of the rest of the object model is complete (see the referenced * issues in ORCJAVA-190) this fix is also redundant. */ private Map<Integer, Element> xmlElementCache = new HashMap<Integer, Element>(); /** * Model builders are sequences of actions to construct the controller's object model (a.k.a * strategy pattern). Different model builders may therefore act on differently * structured XML document instances. <p> * * Model builder implementations in general delegate sub-tasks to various object builders * that have been registered for the relevant XML schema. <p> * * This model builder's lifecycle is delimited by the controller's soft restart * lifecycle (see {@link #softRestart()}. Each deploy lifecycle represents one object model * (and therefore one model builder instance) that matches a particular XML schema structure. * * @see ModelBuilder * @see #softRestart */ private ModelBuilder modelBuilder = null; /** * This is a file watch service for the controller definition associated with the current * object model builder (i.e. the currently deployed controller XML schema). <p> * * Depending on the implementation (deletegated to current model builder instance) it may * detect changes from the file's timestamp, adding/deleting particular files, etc. and * control the deployer lifecycle accordingly, initiating soft restarts, shutdowns, and so on. */ private ControllerDefinitionWatch controllerDefinitionWatch; /** * This is a generic state flag indicator for this deployer service that its operations are * in a 'paused' state -- these may occur during periods where the internal object model is * changed, such as during a soft restart. <p> * * Method implementations in this class may use this flag to check whether to service the * incoming call, block it until pause flag is cleared, or immediately return back to the * caller. */ private boolean isPaused = false; /** * Flag to indicate if the controller has been started (is ready to deploy controller * definitions). <p> * * This is used internally to enforce API call requirements of {@link #startController} * (should only be called once). */ private boolean started = false; /** * Human readable service name for this deployer service. Useful for some logging and * diagnostics. */ private String name = "<undefined>"; /** * Reference to controller database object from Beehive. This object also links to the AccountDTO * which can be used to perform automated deploy features and is needed for device discovery. */ private ControllerDTO controllerDTO; /** * This list holds all discovered devices which are not announced to beehive yet */ protected List<DiscoveredDeviceDTO> discoveredDevicesToAnnounce = new ArrayList<DiscoveredDeviceDTO>(); /** * Reference to the thread handling the controller announcement notifications */ private ControllerAnnouncement controllerAnnouncement; /** * Reference to the thread handling the announcement of discovered devices */ private DiscoveredDevicesAnnouncement discoveredDevicesAnnouncement; /** * Reference to the service which checks Beehive regularly for any new actions that this controller should perform<br> * For example unlink from Beehive, download new design, start proxy, update controller, .... */ private BeehiveCommandCheckService beehiveCommandCheckService; // Constructors --------------------------------------------------------------------------------- /** * Creates a new deployer service with a given device state cache implementation and user * configuration variables. <p> * * Creating a deployer instance will not make it 'active' -- no controller object model is * loaded (or attempted to be loaded) until a {@link #startController()} method is called. * The <tt>startController</tt> therefore acts as an initializer method for the controller * runtime. * * @see #startController() * * @param serviceName human-readable name for this deployer service * @param deviceStateCache device cache instance for this deployer * @param controllerConfig user configuration of this deployer's controller * @param builders model builder implementations (controller schema versions) * available for this deployer * * @throws InitializationException if an unrecognized model builder schema version has * been configured for this deployer */ public Deployer(String serviceName, StatusCache deviceStateCache, ControllerConfiguration controllerConfig, Map<String, ModelBuilder> builders) throws InitializationException { if (deviceStateCache == null || controllerConfig == null) { throw new IllegalArgumentException("Null parameters are not allowed."); } this.deviceStateCache = deviceStateCache; this.controllerConfig = controllerConfig; this.builders = createTypeSafeBuilderMap(builders); if (name != null) { this.name = serviceName; } this.controllerDefinitionWatch = new ControllerDefinitionWatch(this); log.debug("Deployer ''{0}'' initialized.", name); } // Public Instance Methods ---------------------------------------------------------------------- /** * This method initializes the controller's runtime model, making it 'active'. The method should * be called once during the lifecycle of the controller JVM -- subsequent re-deployments of * controller's runtime should go via {@link #softRestart()} method. <p> * * If a controller definition is present, it is loaded and the object model created accordingly. * If no definition is found, the controller is left in an init state where adding the * required artifacts to the controller will trigger the deployment of controller definition. * * @see #softRestart() */ public synchronized void startController() { if (started) { log.error("Method startController() should only be called once per VM process. Use softRestart() " + "for hot-deploying new controller state."); return; } if (controllerConfig.getBeehiveSyncing()) { //Start controller announcement thread controllerAnnouncement = new ControllerAnnouncement(this); controllerAnnouncement.start(); //Start discovered devices announcement thread discoveredDevicesAnnouncement = new DiscoveredDevicesAnnouncement(this); discoveredDevicesAnnouncement.start(); } try { startup(); } catch (ControllerDefinitionNotFoundException e) { log.info("\n\n" + "********************************************************************************\n" + "\n" + " Controller definition was not found in this OpenRemote Controller instance. \n" + "\n" + " If you are starting the controller for the first time, please use your web \n" + " browser to connect to the controller home page and synchronize it with your \n" + " online account. \n" + "\n" + "********************************************************************************\n\n" + "\n" + e.getMessage()); } catch (Throwable t) { log.error("!!! CONTROLLER STARTUP FAILED : {0} !!!", t, t.getMessage()); } controllerDefinitionWatch.start(); started = true; // TODO : ORCJAVA-180, register shutdown hook } /** * Indicates the current state of the deployer. Deployer may be 'paused' during certain * lifecycle stages, such as reloading the controller's internal object model. During those * phases, other deployer operations may opt to block calling threads until deployer has * resumed, or return calls immediately without servicing them. * * @return true to indicate deployer is currently paused, false otherwise */ public boolean isPaused() { // TODO : // the use of this method is restricted to REST API implementation and the original use looks // dubious -- once those parts of REST API have been reworked, this method may be candidate // for removal. return isPaused; } /** * Initiate a shutdown/startup sequence. <p> * * Shutdown phase will undeploy the current runtime object model and manage service lifecycles * that are dependent on the object model. Resources will be stopped and freed. <p> * * The soft restart only impacts the runtime object model and associated component lifecycles * in the controller. The controller itself stays at an init level where a new object model can * be loaded into the system. The JVM process will not exit. <p> * * Startup phase loads back a runtime object model and starts dependent services of the controller. * The object model is loaded from the controller definition file, path of which is indicated by * the {@link org.openremote.controller.ControllerConfiguration#getResourcePath()} method. <p> * * After the startup phase is done, a complete functional controller definition has been * loaded into the controller (unless fatal errors occured), it has been initialized and * started and it is ready to handle incoming requests. <p> * * <b>NOTE : </b> This method call should only be used after {@link #startController()} * has been invoked once. The <tt>startController</tt> method will initialize this deployer * instance's lifecycle, and perform first deployment of controller definition, if the * required artifacts are present in the controller. </p> * * Subsequent soft restarts of this controller/deployer should use this method. * * @see #startController() * @see org.openremote.controller.ControllerConfiguration#getResourcePath * @see #softShutdown * @see #startup() * * @throws ControllerDefinitionNotFoundException * If there are no controller definitions to load from. This exception indicates that * the {@link #startup} phase of the restart cannot complete. The controller/deployer * is left in an init state where the previous controller object model has been * undeployed, and a new one will be deployed once the required artifacts have been * added to the controller. */ public void softRestart() throws ControllerDefinitionNotFoundException { // TODO : ORCJAVA-184 -- queue concurrent calls try { pause(); softShutdown(); startup(); } finally { resume(); } } /** * Deploys a controller configuration from a given ZIP archive. This can be used when the * controller configuration is already present on the local system. <p> * * The contents of the ZIP archive will be extracted on the file system location pointed * by the 'resource.path' property. <p> * * If the {@link ControllerConfiguration#RESOURCE_UPLOAD_ALLOWED} has been configured to * 'false', this method will throw an exception. * * @see org.openremote.controller.ControllerConfiguration#getResourcePath() * @see org.openremote.controller.ControllerConfiguration#isResourceUploadAllowed() * * @see #deployFromOnline(String, String) * * @param inputStream Input stream to the zip file to deploy. Note that this method will * attempt to close the input stream on exit. * * @throws ConfigurationException If new deployments through admin interface have been disabled, * or if the target path to extract the new deployment archive * cannot be resolved, or if the target path does not exist * * @throws IOException If there was an unrecovable I/O error when extracting the * deployment archive. Note that errors in extracting individual * files from within the deployment archive may be logged as * errors or warnings instead of raising an exception. */ public void deployFromZip(InputStream inputStream) throws ConfigurationException, IOException { // TODO: // May need a proper permissions object -- this would allow specific // username/credentials to upload only. Maybe there would be some benefit // in having this master all on/off check also implemented as a permission // rather than configuration option. Dunno. // [JPL] if (!controllerConfig.isResourceUploadAllowed()) { throw new ConfigurationException("Updating controller through web interface has been disabled. " + "You must update controller files manually instead."); } String resourcePath = controllerConfig.getResourcePath(); if (resourcePath == null || resourcePath.equals("")) { throw new ConfigurationException( "Configuration option 'resource.path' was not found or contains empty value."); } URI resourceDirURI = new File(controllerConfig.getResourcePath()).toURI(); unzip(inputStream, resourceDirURI); copyLircdConf(resourceDirURI, controllerConfig); } /** * Deploys a controller configuration directly from user's account stored on the backend. * A HTTP connection is created to Beehive server and account information is downloaded to * this controller using the given user name and credentials. * * @see #deployFromZip(java.io.InputStream) * * @param username user name to download account configuration through Beehive's REST * interface * * @param credentials credentials to authenticate to use Beehive's REST interface * * @throws ConfigurationException If the connection to backend cannot be created due to * configuration errors, or deploying new configuration has * been disabled, or there were other configuration errors * which prevented the deployment archive from being extracted. * * @throws ConnectionException If connection creation failed, or reading from the connection * failed for any reason */ public void deployFromOnline(String username, String credentials) throws ConfigurationException, ConnectionException { BeehiveConnection connection = new BeehiveConnection(this); InputStream stream = connection.downloadZip(username, credentials); try { deployFromZip(stream); } catch (IOException e) { throw new ConnectionException("Extracting controller configuration from Beehive account failed : {0}", e, e.getMessage()); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { log.warn("Could not close I/O stream to downloaded user configuration : {0}", e, e.getMessage()); } } } } /** * TODO : * * This is temporarily here, part of the refactoring of deprecating ComponentBuilder and * migrating to a proper ObjectBuilder implementation. The existing component builders * externalize their XML parsing which is currently serviced here. Over the long term, * the object builders should be dependents of model builder which provides XML parsing * services like this method. * * See ORCJAVA-143, ORCJAVA-144, ORCJAVA-151, ORCJAVA-152, ORCJAVA-153, ORCJAVA-155, * ORCJAVA-158, ORCJAVA-182, ORCJAVA-186 * */ public Element queryElementById(int id) throws InitializationException { if (modelBuilder == null) { throw new IllegalStateException("Runtime object model has not been initialized."); } // Quick fix to cache parsed XML elements. See ORCJAVA-190 Element tmp = xmlElementCache.get(id); if (tmp == null) { tmp = ((Version20ModelBuilder) modelBuilder).queryElementById(id); xmlElementCache.put(id, tmp); } return tmp; } /** * TODO * * @return */ public Map<String, String> getConfigurationProperties() { if (modelBuilder == null) { // TODO : See ORCJAVA-183 log.debug("Runtime object model has not been initialized. Using default configuration only."); return new HashMap<String, String>(0); } return ((Version20ModelBuilder) modelBuilder).getConfigurationProperties(); } /** * TODO * * This is temporarily here, part of refactoring to internalize Java-to-XML mapping to their * corresponding services. This is used by deprecated ComponentBuilder implementations * (see ORCJAVA-143) which rely on sensors for state input. * * See ORCJAVA-143, ORCJAVA-144, ORCJAVA-147, ORCJAVA-151, ORCJVA-152, ORCJAVA-153 * * * @param componentIncludeElement JDOM element for sensor * * @throws InitializationException if the sensor model cannot be built from the given XML * element * * @return sensor */ public Sensor getSensorFromComponentInclude(Element componentIncludeElement) throws InitializationException { if (componentIncludeElement == null) { throw new InitializationException("Implementation error, null reference on expected " + "<include type = \"sensor\" ref = \"nnn\"/> element."); } Attribute includeTypeAttr = componentIncludeElement.getAttribute(XMLMapping.XML_INCLUDE_ELEMENT_TYPE_ATTR); String typeAttributeValue = includeTypeAttr.getValue(); if (!typeAttributeValue.equals(XMLMapping.XML_INCLUDE_ELEMENT_TYPE_SENSOR)) { throw new XMLParsingException("Expected to include 'sensor' type, got {0} instead.", typeAttributeValue); } Attribute includeRefAttr = componentIncludeElement.getAttribute(XMLMapping.XML_INCLUDE_ELEMENT_REF_ATTR); String refAttributeValue = includeRefAttr.getValue(); try { int sensorID = Integer.parseInt(refAttributeValue); return getSensor(sensorID); } catch (NumberFormatException e) { throw new InitializationException("Currently only integer values are accepted as unique sensor ids. " + "Could not parse {0} to integer.", refAttributeValue); } } /** * Method is called by the commandBuilder of a protocol if the commandBuilders discovers devices for his * protocol that should be announced to Beehive. * * @param list - the list of devices to announce */ public void announceDiscoveredDevices(List<DiscoveredDeviceDTO> list) { synchronized (discoveredDevicesToAnnounce) { discoveredDevicesToAnnounce.addAll(list); } } /** * Method is called by the ConfigManageController which is invoked by index.html * @return a String with the linked account Id or the MAC address with a leading '-' */ public String getLinkedAccountId() throws Exception { if (controllerConfig.getBeehiveSyncing()) { if ((controllerDTO != null) && (controllerDTO.getAccount() != null)) { return controllerDTO.getAccount().getOid().toString(); } else { return "-" + BeehiveCommandCheckService.getMACAddresses(); } } else { return "no"; } } // Protected Instance Methods ------------------------------------------------------------------- /** * Returns a registered sensor instance. Sensor instances are shared across threads. * Retrieving a sensor with the same ID will yield a same instance. <p> * * If the sensor with given ID is not found, a <tt>null</tt> is returned. * * @see org.openremote.controller.model.sensor.Sensor#getSensorID() * * @param id sensor ID * * @return sensor instance, or null if sensor with given ID was not found */ protected Sensor getSensor(int id) { // TODO : // This is currently used by getSensorFromComponentInclude() which is a temporary method // on this interface, as part of ongoing refactoring. This method may not need to stay // once those refactorings are complete. Also notice that this method is currently the // only consumer of the deviceStateCache.getSensor() call. // // The visibility could be lowered to private once unit tests do not directly use this // API anymore -- they could instead use the StatusCache API. Sensor sensor = deviceStateCache.getSensor(id); if (sensor == null) { // Write a log entry on accessing sensor ID that does not exist. At the moment letting // this continue as it is, that is, returning a null pointer which is likely to blow up // elsewhere unless the calling code guards against it. May consider other ways of // handling it later. // [JPL] log.error("Attempted to access sensor with id ''{0}'' which did not exist in device " + "state cache.", id); } return sensor; } /** * Retrieves the user's login name from user file. * * @return user's login name or an empty string if login name was not found or there * was an error reading it */ protected String getUserName() { // TODO : // moved temporarily into public Deployer API to satisfy requirements to Beehive // command check service -- should move back when the Beehive command check is // part of the Beehive Connection implementation where it belongs File user = new File(getUserFileLocation()); BufferedReader reader = null; try { if (!user.exists()) // TODO privileged block { return ""; } reader = new BufferedReader(new FileReader(user)); return reader.readLine(); } catch (IOException e) { log.error("Can't read login name due to I/O error : {0}", e, e.getMessage()); return ""; } catch (SecurityException e) { log.error("Security manager has prevented access to user's login name: {0}", e, e.getMessage()); return ""; } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { log.warn("Unable to close file ''{0}''", getUserFileLocation()); } } } } /** * Retrieves user's password from controller's keystore. * * @param username user's login name * * @return * * @throws KeyManager.KeyManagerException * if there was an error accessing the keystore * * @throws PasswordManager.PasswordNotFoundException * if the password for the user was not found in the keystore */ protected String getPassword(String username) throws PasswordException { // TODO : // moved temporarily into public Deployer API to satisfy requirements to Beehive // command check service -- should move back when the Beehive command check is // part of the Beehive Connection implementation where it belongs byte[] credentials = null; if (controllerConfig.getSecurityPasswordStorage() == SecurityPasswordStorage.PLAINTEXT) { if (getPasswordFileLocation().getScheme().startsWith("file")) { File f = new File(getPasswordFileLocation()); if (!f.exists()) { throw new PasswordException( "User credentials were not present, please synchronize the controller with your " + "OpenRemote Designer/Beehive account first."); } BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(f)); credentials = reader.readLine().getBytes(Charset.forName("UTF-8")); } catch (IOException e) { log.error("Can't read password due to I/O error : {0}", e, e.getMessage()); } catch (SecurityException e) { log.error("Security manager has prevented access to user's password: {0}", e, e.getMessage()); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { log.warn("Unable to close file ''{0}''", getPasswordFileLocation()); } } } } } else { if (getKeyStoreLocation().getScheme().startsWith("file")) { File f = new File(getKeyStoreLocation()); if (!f.exists()) { throw new PasswordException( "User credentials were not present, please synchronize the controller with your " + "OpenRemote Designer/Beehive account first."); } } try { PasswordManager pw = new PasswordManager(getKeyStoreLocation(), (getSystemUser() + ".key").toCharArray()); credentials = pw.getPassword(username, (getSystemUser() + ".key").toCharArray()); } catch (PasswordManager.PasswordNotFoundException e) { throw new PasswordException("Password for user ''{0}'' was not found", e, username, e.getMessage()); } catch (KeyManager.KeyManagerException e) { throw new PasswordException("Error accessing user's password: {0}", e, e.getMessage()); } } if (credentials == null) { throw new PasswordException( "User credentials could not be retrieved, please synchronize the controller with your " + "OpenRemote Designer/Beehive account first."); } return new String(credentials, Charset.forName("UTF-8")); } // Private Instance Methods --------------------------------------------------------------------- /** * Implements the sequence of shutting down the currently deployed controller * runtime (but will not exit the VM process). <p> * * After this method completes, the controller has no active runtime object model but is * at an init level where a new one can be loaded in. */ private void softShutdown() { log.info("\n\n" + "--------------------------------------------------------------------\n\n" + " UNDEPLOYING CURRENT CONTROLLER RUNTIME...\n\n" + "--------------------------------------------------------------------\n"); // TODO : ORCJAVA-188, introduce lifecycle management for event processors // TODO : ORCJAVA-188, introduce lifecycle management for connection managers // TODO : ORCJAVA-188, introduce and use generic LifeCycle interface for state cache if (beehiveCommandCheckService != null) { beehiveCommandCheckService.stop(); } deviceStateCache.shutdown(); controllerXMLElementCache.clear(); xmlElementCache.clear(); modelBuilder = null; // null here indicates to other services that this deployer // installer currently has no object model deployed log.info("Shutdown complete."); } /** * Manages the build-up of controller runtime with object model creation * from the XML document instance(s). Attempts to detect from configuration files * which version of object model and corresponding XML schema should be used to * build the runtime model. <p> * * Once this method returns, the controller runtime is 'ready' -- that is, the object * model has been created and also initialized, registered and started so it is fully * functional and able to receive requests. <p> * * Note that partial failures (such as errors in the controller's object model definition) may * not prevent the startup from completing. Such errors may be logged instead, leaving the * controller with an object model that is only partial from the intended one. * * @throws ControllerDefinitionNotFoundException * If the startup could not be completed because no controller definition * was found. */ private void startup() throws ControllerDefinitionNotFoundException { try { ModelBuilder.SchemaVersion version = detectVersion(); log.info("\n\n" + "--------------------------------------------------------------------\n\n" + " DEPLOYING NEW CONTROLLER RUNTIME...\n\n" + "--------------------------------------------------------------------\n"); switch (version) { case VERSION_3_0: modelBuilder = builders.get(ModelBuilder.SchemaVersion.VERSION_3_0); break; case VERSION_2_0: modelBuilder = builders.get(ModelBuilder.SchemaVersion.VERSION_2_0); break; default: throw new Error("Unrecognized schema version " + version); } // NOTE: the schema 2.0 builder auto-starts all the sensors it locates in the // controller.xml definition // // TODO: ORCJAVA-188 // generalizing the sensor start mechanism through a lifecycle interface -- // the builder should register the sensors and leave the lifecycle management // to the managing framework modelBuilder.buildModel(); Map<String, String> props = getConfigurationProperties(); controllerConfig.setConfigurationProperties(props); } finally { if (beehiveCommandCheckService != null) { beehiveCommandCheckService.stop(); } beehiveCommandCheckService = new BeehiveCommandCheckService(controllerConfig); beehiveCommandCheckService.start(this); log.info("Startup complete."); } } /** * Sets this deployer in 'paused' state. * * @see #resume */ private void pause() { isPaused = true; // TODO : ORCJAVA-188 -- pause() candidate for more generic lifecycle interface controllerDefinitionWatch.pause(); } /** * Resumes this deployer from a previously 'paused' state. * * @see #pause */ private void resume() { try { // TODO : ORCJAVA-188 -- resume() candidate for more generic lifecycle interface controllerDefinitionWatch.resume(); } finally { isPaused = false; } } /** * A simplistic attempt at detecting which schema version we should use to build * the object model. * * TODO -- MODELER-256, ORCJAVA-189 : xml schema should include explicit version info * * @return the detected schema version * * @throws ControllerDefinitionNotFoundException * if we can't find any controller definition files to load */ private ModelBuilder.SchemaVersion detectVersion() throws ControllerDefinitionNotFoundException { // Check if 3.0 schema instance is in place (TODO : this doesn't actually exist yet...) if (Version30ModelBuilder.checkControllerDefinitionExists(controllerConfig)) { return ModelBuilder.SchemaVersion.VERSION_3_0; } // Check for 2.0 schema instance... if (Version20ModelBuilder.checkControllerDefinitionExists(controllerConfig)) { return ModelBuilder.SchemaVersion.VERSION_2_0; } // TODO - update message below once 3.0 is in place throw new ControllerDefinitionNotFoundException( "Could not find a controller definition to load at path ''{0}'' (for version 2.0)", Version20ModelBuilder.getControllerDefinitionFile(controllerConfig)); } /** * Extracts an OpenRemote deployment archive into a given target file directory. * * @param inputStream Input stream for reading the ZIP archive. Note that this method will * attempt to close the stream on exiting. * * @param targetDir URI that points to the root directory where the extracted files should * be placed. Note that the URI must be an absolute file URI. * * @throws ConfigurationException If target file URI cannot be resolved, or if the target * file path does not exist * * @throws IOException If there was an unrecovable I/O error reading or extracting * the ZIP archive. Note that errors on individual files within * the archive may not generate exceptions but be logged as * errors or warnings instead. */ private void unzip(InputStream inputStream, URI targetDir) throws ConfigurationException, IOException { if (targetDir == null || targetDir.getPath().equals("") || !targetDir.isAbsolute()) { throw new ConfigurationException( "Target dir must be absolute file: protocol URI, got '" + targetDir + +'.'); } File checkedTargetDir = new File(targetDir); if (!checkedTargetDir.exists()) { throw new ConfigurationException("The path ''{0}'' doesn't exist.", targetDir); } ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(inputStream)); ZipEntry zipEntry = null; BufferedOutputStream fileOutputStream = null; try { while ((zipEntry = zipInputStream.getNextEntry()) != null) { if (zipEntry.isDirectory()) { continue; } try { URI extractFileURI = targetDir.resolve(new URI(null, null, zipEntry.getName(), null)); log.debug("Resolved URI to ''{0}''", extractFileURI); File zippedFile = new File(extractFileURI); log.debug("Attempting to extract ''{0}'' to ''{1}''.", zipEntry, zippedFile); try { fileOutputStream = new BufferedOutputStream(new FileOutputStream(zippedFile)); int b; while ((b = zipInputStream.read()) != -1) { fileOutputStream.write(b); } } catch (FileNotFoundException e) { log.error("Could not extract ''{0}'' -- file ''{1}'' could not be created : {2}", e, zipEntry.getName(), zippedFile, e.getMessage()); } catch (IOException e) { log.warn("Zip extraction of ''{0}'' to ''{1}'' failed : {2}", e, zipEntry, zippedFile, e.getMessage()); } finally { if (fileOutputStream != null) { try { fileOutputStream.close(); log.debug("Extraction of ''{0}'' to ''{1}'' completed.", zipEntry, zippedFile); } catch (Throwable t) { log.warn("Failed to close file ''{0}'' : {1}", t, zippedFile, t.getMessage()); } } if (zipInputStream != null) { if (zipEntry != null) { try { zipInputStream.closeEntry(); } catch (IOException e) { log.warn("Failed to close ZIP file entry ''{0}'' : {1}", e, zipEntry, e.getMessage()); } } } } } catch (URISyntaxException e) { log.warn("Cannot extract {0} from zip : {1}", e, zipEntry, e.getMessage()); } } } finally { try { if (zipInputStream != null) { zipInputStream.close(); } } catch (IOException e) { log.warn("Failed to close zip file : {0}", e, e.getMessage()); } if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException e) { log.warn("Failed to close file : {0}", e, e.getMessage()); } } } } /** * TODO * * @param resourcePath * @param config */ private void copyLircdConf(URI resourcePath, ControllerConfiguration config) { File lircdConfFile = new File(resourcePath.resolve(Constants.LIRCD_CONF).getPath()); File lircdconfDir = new File(config.getLircdconfPath().replaceAll(Constants.LIRCD_CONF, "")); try { if (lircdconfDir.exists() && lircdConfFile.exists()) { // this needs root user to put lircd.conf into /etc. // because it's readonly, or it won't be modified. if (config.isCopyLircdconf()) { FileUtils.copyFileToDirectory(lircdConfFile, lircdconfDir); log.info("copy lircd.conf to" + config.getLircdconfPath()); } } } catch (IOException e) { log.error("Can't copy lircd.conf to " + config.getLircdconfPath(), e); } } /** * This is a helper method that translates the string based version keys from the DI * configuration to typesafe enums. * * @param builders the model builder implementations (one per schema) configured for * this deploer * * @return builder map with a schema as key and a model builder Java object * instance as value * * * @see ModelBuilder.SchemaVersion#toSchemaVersion(String) * * @throws InitializationException if an unrecognized model builder schema version has been * configured for this deployer */ private Map<ModelBuilder.SchemaVersion, ModelBuilder> createTypeSafeBuilderMap( Map<String, ModelBuilder> builders) throws InitializationException { Map<ModelBuilder.SchemaVersion, ModelBuilder> map = new HashMap<ModelBuilder.SchemaVersion, ModelBuilder>(); for (String key : builders.keySet()) { ModelBuilder.SchemaVersion schema = ModelBuilder.SchemaVersion.toSchemaVersion(key); ModelBuilder builder = builders.get(key); builder.setDeployer(this); //We inject the deployer manually, since Spring has a problem with circular dependencies map.put(schema, builder); } return map; } /** * Returns the URI to the keystore file in the controller. The .keystore file will be stored * in the location defined by * {@link org.openremote.controller.ControllerConfiguration#getResourcePath()}. <p> * * This implementation assumes the configured resource path location can be converted to a * correctly formatted file URI. * * @return URI pointing to a keystore file location */ private URI getKeyStoreLocation() { File resourceDir = new File(controllerConfig.getResourcePath()); return new File(resourceDir, ".keystore").toURI(); } /** * Returns the URI to the password file in the controller. The .password file will be stored * in the location defined by * {@link org.openremote.controller.ControllerConfiguration#getResourcePath()}. * * A password file is used when password security is configured to use plain text. * * This implementation assumes the configured resource path location can be converted to a * correctly formatted file URI. * * @return URI pointing to a password file location */ private URI getPasswordFileLocation() { File resourceDir = new File(controllerConfig.getResourcePath()); return new File(resourceDir, ".password").toURI(); } /** * Returns an URI to the user login name file in the controller. The .user file will be stored * in the location defined by * {@link org.openremote.controller.ControllerConfiguration#getResourcePath()}. <p> * * This implementation assumes the configured resource path location can be converted to a * correctly formatted file URI. * * @return URI pointing to a file containing current user's login name */ private URI getUserFileLocation() { // IMPLEMENTATION NOTE: // This file name is being used in privileged code blocks so therefore the method should // not be parameterized and should always return a single fixed file location. File resourceDir = new File(controllerConfig.getResourcePath()); return new File(resourceDir, ".user").toURI(); } /** * Returns the system user login name, or an empty string if access to system user information * has been denied. * * @return system user login name or empty string */ private String getSystemUser() { try { // ----- BEGIN PRIVILEGED CODE BLOCK ------------------------------------------------------ return AccessController.doPrivilegedWithCombiner(new PrivilegedAction<String>() { @Override public String run() { return System.getProperty("user.name"); } }); // ----- END PRIVILEGED CODE BLOCK -------------------------------------------------------- } catch (SecurityException e) { log.info("Security manager has denied access to user login name: {0}", e, e.getMessage()); return ""; } } protected String encodeKey(String username, byte[] password) { Md5PasswordEncoder encoder = new Md5PasswordEncoder(); return encoder.encodePassword(new String(password), username); } // Nested Classes ------------------------------------------------------------------------------- /** * This service performs the automated file watching of the controller definition artifacts * (depending on the model builder implementation). <p> * * Per the rules defined in this implementation and in combination with those provided by * model builders via their * {@link org.openremote.controller.deployer.ModelBuilder#hasControllerDefinitionChanged()} * method implementations, this service controls the deployer lifecycle through * {@link org.openremote.controller.service.Deployer#softRestart()} and * {@link Deployer#softShutdown()} methods. * * @see org.openremote.controller.service.Deployer#softRestart() * @see org.openremote.controller.service.Deployer#softShutdown() */ private static class ControllerDefinitionWatch implements Runnable { // TODO : ORCJAVA-188 -- should implement lifecycle interface // Instance Fields ---------------------------------------------------------------------------- /** * Deployer reference for this service to control the deployer lifecycle. */ private Deployer deployer; /** * Indicates the watcher thread is running. */ private volatile boolean running = true; /** * Indicates the watcher thread should temporarily pause and not trigger any actions * on the deployer. */ private volatile boolean paused = false; /** * The actual thread reference. */ private Thread watcherThread; // Constructors ------------------------------------------------------------------------------- /** * Creates a new controller file watcher for a given deployer. Use {@link #start()} to * make this service active (start the relevant thread(s)). * * @param deployer reference to the deployer whose lifecycle this watcher service controls */ private ControllerDefinitionWatch(Deployer deployer) { this.deployer = deployer; } // Instance Methods --------------------------------------------------------------------------- /** * Starts the controller definition watcher thread. */ public void start() { watcherThread = OpenRemoteRuntime .createThread("Controller Definition File Watcher for " + deployer.name, this); watcherThread.start(); log.info("{0} started.", watcherThread.getName()); } /** * Stops (and kills) the controller definition watcher thread. */ public void stop() { running = false; watcherThread.interrupt(); } /** * Temporarily pauses the controller definition watcher thread, preventing any state * modifications to the associated deployment service. * * @see #resume */ public void pause() { paused = true; } /** * Resumes the controller definition watcher thread after it has been {@link #pause() paused}. * * @see #pause */ public void resume() { paused = false; } // Implements Runnable ------------------------------------------------------------------------ /** * Runs the watcher thread using the following logic: <p> * * - If paused, do nothing <br> * * - If cannot detect controller definition files for any known schemas, keep waiting <br> * * - If detects a controller definition has been added but not deployed, run * deployer.softRestart() <br> * * - If has an existing controller object model deployed but the model builder reports * a change in it (what constitutes a change depends on deployed model builder implementation), * then run deployer.softRestart() <br> * * - If an existing controller model was deployed but the controller definition is removed * (as reported by {@link org.openremote.controller.service.Deployer#detectVersion()}) * then undeploy the object model. */ @Override public void run() { while (running) { if (paused) continue; try { deployer.detectVersion(); // will throw an exception if no known schemas are found... if (deployer.modelBuilder == null || deployer.modelBuilder.hasControllerDefinitionChanged()) { try { deployer.softRestart(); } catch (ControllerDefinitionNotFoundException e) { log.error("Soft restart cannot complete, controller definition not found : {0}", e.getMessage()); } catch (Throwable t) { log.error("Controller soft restart failed : {0}", t, t.getMessage()); } } } catch (ControllerDefinitionNotFoundException e) { if (deployer.modelBuilder != null) { deployer.softShutdown(); } else { log.trace("Did not locate controller definitions for any known schema..."); } } try { Thread.sleep(2000); } catch (InterruptedException e) { running = false; Thread.currentThread().interrupt(); } } log.info("{0} has been stopped.", watcherThread.getName()); } } /** * Abstracts the connectivity from controller to back-end in this nested class. <p> * * Currently only the deployer is making connections to backend. This will be later * expanded with local device discovery data import, log analytics, data collection * history, remote access and other operations. At that point this class visibility may * be expanded and made more generic. */ private static class BeehiveConnection { /** * Part of the Beehive URL to retrieve user's controller configuration from their * account. <p> * * The complete URL should be : * [Beehive REST Base URL]/BEEHIVE_REST_USER_DIR/[username]/BEEHIVE_REST_OPENREMOTE_ZIP */ private final static String BEEHIVE_REST_USER_DIR = "user"; /** * Part of the Beehive URL to retrieve user's controller configuration from their * account. <p> * * The complete URL should be : * [Beehive REST Base URL]/BEEHIVE_REST_USER_DIR/[username]/BEEHIVE_REST_OPENREMOTE_ZIP */ private final static String BEEHIVE_REST_OPENREMOTE_ZIP = "openremote.zip"; /** * Reference to the deployer that uses this connection. */ private Deployer deployer; // Constructors ------------------------------------------------------------------------------- /** * Constructs a new connection object for a given deployer. * * @param deployer reference to the deployer instance that owns this connection */ private BeehiveConnection(Deployer deployer) { this.deployer = deployer; } // Instance Methods --------------------------------------------------------------------------- /** * Downloads user's configuration from Beehive using the user's account name and credentials. * * @param username user's account name -- part of the HTTP GET URL used to retrieve the * account configuration * @param credentials user's credentials to access their account * * @return I/O stream to read the incoming ZIP file from user's account. Note that it is up * to the caller to close the stream when appropriate. The incoming stream has basic * buffering for read operations enabled. * * @throws ConfigurationException if the connection to backend cannot be created due to * configuration errors in the controller * * @throws ConnectionException if the connection creation fails for any reason */ private InputStream downloadZip(String username, String credentials) throws ConfigurationException, ConnectionException { // TODO : // Could eventually return a list of URLs if multiple backend targets are enabled which // can be used for transparent failover. See ORCJAVA-191. String beehiveBase = deployer.controllerConfig.getBeehiveRESTRootUrl(); String httpURI = BEEHIVE_REST_USER_DIR + "/" + username + "/" + BEEHIVE_REST_OPENREMOTE_ZIP; try { URL beehiveBaseURL = new URL(beehiveBase); URI beehiveUserURI = beehiveBaseURL.toURI().resolve(httpURI); // TODO : Should force to go over HTTPS always. See ORCJAVA-192. URLConnection connection = beehiveUserURI.toURL().openConnection(); if (!(connection instanceof HttpURLConnection)) { throw new ConfigurationException( "The ''{0}'' property ''{1}'' must be a URL with http:// schema.", ControllerConfiguration.BEEHIVE_REST_ROOT_URL, beehiveBase); } HttpURLConnection http = (HttpURLConnection) connection; http.setDoInput(true); http.setRequestMethod("GET"); http.addRequestProperty(Constants.HTTP_AUTHORIZATION_HEADER, HttpUtils.generateHttpBasicAuthorizationHeader(username, credentials)); http.connect(); int response = http.getResponseCode(); switch (response) { case HttpURLConnection.HTTP_OK: // if authentication was ok, store user's credentials for background API calls... storeCredentials(username, credentials.getBytes()); return new BufferedInputStream(http.getInputStream()); case HttpURLConnection.HTTP_UNAUTHORIZED: case HttpURLConnection.HTTP_NOT_FOUND: throw new ConnectionException( "Authentication failed, please check your username and password."); default: throw new ConnectionException("Connection to ''{0}'' failed, HTTP error code {1} - {2}", beehiveUserURI, response, http.getResponseMessage()); } } catch (MalformedURLException e) { throw new ConfigurationException( "Configuration property ''{0}'' with value ''{1}'' is not a valid URL : {2}", e, ControllerConfiguration.BEEHIVE_REST_ROOT_URL, beehiveBase, e.getMessage()); } catch (URISyntaxException e) { throw new ConfigurationException("Invalid URI : {0}", e, e.getMessage()); } catch (ProtocolException e) { throw new ConnectionException("Failed to create HTTP request : {0}", e, e.getMessage()); } catch (IOException e) { throw new ConnectionException("Downloading account configuration failed : {0}", e, e.getMessage()); } } /** * Stores currently authenticated user's credentials in a keystore. * * @param username user's login name * @param credentials user's credentials */ private void storeCredentials(String username, byte[] credentials) { File userfile = new File(deployer.getUserFileLocation()); BufferedWriter userWriter = null; try { // If there was a previous user login file, delete it... deleteUserFile(); // Write the current user login name to the file... userWriter = new BufferedWriter(new FileWriter(userfile)); userWriter.write(username); userWriter.close(); } catch (IOException e) { log.error("Unable to save current user login name. Background API requests will not " + "be able to authenticate: {0}", e, e.getMessage()); try { // clean up if there was any left-overs... deleteUserFile(); } catch (IOException io) { log.warn("Could not delete existing file ''{0}'' due to I/O error: {1}", io, userfile, io.getMessage()); } } finally { if (userWriter != null) { try { userWriter.close(); } catch (IOException e) { log.warn("Could not close file ''{0}'': {1}", e, userfile, e.getMessage()); } } } if (deployer.controllerConfig.getSecurityPasswordStorage() == SecurityPasswordStorage.PLAINTEXT) { File passwordFile = new File(deployer.getPasswordFileLocation()); BufferedWriter passwordWriter = null; try { // If there was a previous password login file, delete it... deletePasswordFile(); // Write the current user login name to the file... passwordWriter = new BufferedWriter(new FileWriter(passwordFile)); passwordWriter.write(new String(credentials, Charset.forName("UTF-8"))); passwordWriter.close(); } catch (IOException e) { log.error("Unable to save current user password. Background API requests will not " + "be able to authenticate: {0}", e, e.getMessage()); try { // clean up if there was any left-overs... deletePasswordFile(); } catch (IOException io) { log.warn("Could not delete existing file ''{0}'' due to I/O error: {1}", io, passwordFile, io.getMessage()); } } finally { if (passwordWriter != null) { try { passwordWriter.close(); } catch (IOException e) { log.warn("Could not close file ''{0}'': {1}", e, passwordFile, e.getMessage()); } } } } else { // Use a key to access the keystore. Note that this is not a secret key, access to // the keystore file itself must be made secure by the host system. // // TODO : allow configuration of retrieving the store key from other locations String storeKey = deployer.getSystemUser() + ".key"; URI keystoreLocation = deployer.getKeyStoreLocation(); try { // Create a keystore or load an existing one if already present... PasswordManager pw = new PasswordManager(keystoreLocation, storeKey.toCharArray()); // TODO : make sure previous user's key will also get removed... // Add current user's credentials to keystore (delete previous one if existed)... pw.removePassword(username, storeKey.toCharArray()); pw.addPassword(username, credentials, storeKey.toCharArray()); } catch (SecurityException e) { log.error("Security manager has prevented saving user login name. Background API requests " + "will not be able to authenticate: {0}", e, e.getMessage()); try { // clean up if there was any left-overs... deleteUserFile(); } catch (IOException io) { log.warn("Could not delete existing file ''{0}'' due to I/O error: {1}", io, userfile, io.getMessage()); } } catch (KeyManager.KeyManagerException e) { log.error("Unable to store user credentials. Background API requests will not be able to " + "authenticate: {0}", e, e.getMessage()); } } } /** * Handles the file I/O and security on deleting the user file containing user's login name. * * @throws IOException if security manager denies access to delete the user file */ private void deleteUserFile() throws IOException { final File userfile = new File(deployer.getUserFileLocation()); try { // ---- BEGIN PRIVILEGED CODE BLOCK ------------------------------------------------------- boolean success = AccessController.doPrivilegedWithCombiner(new PrivilegedAction<Boolean>() { @Override public Boolean run() { if (userfile.exists()) { return userfile.delete(); } else { return true; } } }); // ---- END PRIVILEGED CODE BLOCK --------------------------------------------------------- if (!success) { log.error("Cannot store user credentials. Unable to delete existing .user file. " + "Background API requests will not be able to authenticate."); } } catch (SecurityException e) { throw new IOException("Security manager has denied access to user file at '" + deployer.getUserFileLocation() + "': " + e.getMessage(), e); } } /** * Handles the file I/O and security on deleting the password file containing user's password. * * @throws IOException if security manager denies access to delete the password file */ private void deletePasswordFile() throws IOException { final File passwordFile = new File(deployer.getPasswordFileLocation()); try { // ---- BEGIN PRIVILEGED CODE BLOCK ------------------------------------------------------- boolean success = AccessController.doPrivilegedWithCombiner(new PrivilegedAction<Boolean>() { @Override public Boolean run() { if (passwordFile.exists()) { return passwordFile.delete(); } else { return true; } } }); // ---- END PRIVILEGED CODE BLOCK --------------------------------------------------------- if (!success) { log.error("Cannot store user credentials. Unable to delete existing .password file. " + "Background API requests will not be able to authenticate."); } } catch (SecurityException e) { throw new IOException("Security manager has denied access to password file at '" + deployer.getPasswordFileLocation() + "': " + e.getMessage(), e); } } } /** * Handles the announcement of the controller via it's MAC address to Beehive.<br> * Once a user has linked this controller to an account and the returned ControllerDTO contains<br> * a AccountDTO with users, this thread is ending, but then the backend command agent is started<br> * to perform a regular check on beehive if a command is there for this controller * * */ private class ControllerAnnouncement extends Thread { private Deployer deployer; private ControllerAnnouncement(Deployer deployer) { super("ControllerAnnouncement"); this.deployer = deployer; } public void run() { // TODO : the lifecycle of this thread wrt deployments needs to be implemented String acctURI = controllerConfig.getBeehiveAccountServiceRESTRootUrl(); if (acctURI == null || acctURI.startsWith("::loopback")) { return; } //As long as we are not linked to an account we periodically try to receive account info while (true) { ClientResource cr = null; Client c = new Client(new Context(), Protocol.HTTPS); try { log.trace("Controller will announce " + BeehiveCommandCheckService.getMACAddresses() + " as MAC address to beehive"); cr = new ClientResource( acctURI + "controller/announce/" + BeehiveCommandCheckService.getMACAddresses()); cr.setNext(c); Representation r = cr.post(null); String str; str = r.getText(); log.trace("Controller announcement received response >" + str + "<"); GenericResourceResultWithErrorMessage res = new JSONDeserializer<GenericResourceResultWithErrorMessage>() .use(null, GenericResourceResultWithErrorMessage.class) .use("result", ControllerDTO.class).deserialize(str); controllerDTO = (ControllerDTO) res.getResult(); if ((controllerDTO != null) && (controllerDTO.getAccount() != null)) { break; } } catch (Exception e) { log.error("!!! Unable to announce controller MAC address to Beehive", e); } finally { if (cr != null) { cr.release(); } } try { Thread.sleep(1000 * 30); } catch (InterruptedException e) { } //Let's wait 30 seconds } } } /** * Handles the announcement of discovered devices.<br> * When discovered devices are available and the controller is linked to an account,<br> * those devices are sent to Beehive. */ private class DiscoveredDevicesAnnouncement extends Thread { private Deployer deployer; DiscoveredDevicesAnnouncement(Deployer deployer) { super("DiscoveredDevicesAnnouncement"); this.deployer = deployer; } public void run() { // TODO : // Guarding against potential timing issues with this thread (the thread implementation // needs to be fixed in general). This thread shouldn't start before a deployment // finishes, so currently starting too early. It's not an issue for now since the // implementation doesn't do anything until items are added to the discovery list // but where such conditions aren't checked, an issue would rise. try { // wait for 10 seconds (arbitrary value) before starting execution... Thread.sleep(10000); } catch (InterruptedException e) { interrupt(); return; } log.info("Starting device discovery service..."); while (true) { log.debug("checking for discovered devices..."); if (!discoveredDevicesToAnnounce.isEmpty()) { synchronized (discoveredDevicesToAnnounce) { ClientResource cr = new ClientResource( controllerConfig.getBeehiveDeviceDiscoveryServiceRESTRootUrl() + "discoveredDevices"); try { String username = getUserName(); if (username == null || username.equals("")) { log.error("Unable to retrieve username for device discovery API call. Skipped..."); } else { cr.setChallengeResponse(ChallengeScheme.HTTP_BASIC, username, getPassword(username)); Representation rep = new JsonRepresentation(new JSONSerializer().exclude("*.class") .deepSerialize(discoveredDevicesToAnnounce)); Representation result = cr.post(rep); cr.release(); GenericResourceResultWithErrorMessage res = new JSONDeserializer<GenericResourceResultWithErrorMessage>() .use(null, GenericResourceResultWithErrorMessage.class) .use("result", ArrayList.class).use("result.values", Long.class) .deserialize(result.getText()); if (res.getErrorMessage() != null) { throw new RuntimeException(res.getErrorMessage()); } discoveredDevicesToAnnounce.clear(); } } catch (Exception e) { log.error("Could not announce discovered devices", e); } } } try { Thread.sleep(1000 * 60); } catch (InterruptedException e) { interrupt(); return; } } } } public void unlinkController() { this.controllerDTO = null; this.controllerAnnouncement = new ControllerAnnouncement(this); controllerAnnouncement.start(); } public static class PasswordException extends OpenRemoteException { public PasswordException(String msg, Object... params) { super(msg, params); } } }