/** * 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.core.thing.binding.firmware; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.Map; import org.apache.commons.io.IOUtils; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.firmware.FirmwareProvider; import org.eclipse.smarthome.core.thing.firmware.FirmwareRegistry; import org.eclipse.smarthome.core.thing.firmware.FirmwareStatusInfo; import org.eclipse.smarthome.core.thing.firmware.FirmwareUpdateService; import org.eclipse.smarthome.core.thing.type.ThingType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; /** * The {@link Firmware} is the description of a firmware to be installed on the physical device of a {@link Thing}. A * firmware relates always to exactly one {@link ThingType}. By its {@link FirmwareUID} it is ensured that there is only * one firmware in a specific version for a thing type available. Firmwares can be easily created by the * {@link Firmware.Builder}. * </p> * Firmwares are made available to the system by {@link FirmwareProvider}s that are tracked by the * {@link FirmwareRegistry}. The registry can be used to get a dedicated firmware or to get all available firmwares for * a specific {@link ThingType}. * </p> * The {@link FirmwareUpdateService} is responsible to provide the current {@link FirmwareStatusInfo} of a thing. * Furthermore this service is the central instance to start a firmware update process. In order that the firmware of a * thing can be updated the hander of the thing has to implement the {@link FirmwareUpdateHandler} interface. * </p> * The {@link Firmware} implements the {@link Comparable} interface in order to be able to sort firmwares based on their * versions. Firmwares are sorted in a descending sequence, i.e. that the latest firmware will be the first * element in a sorted result set. The implementation of {@link Firmware#compareTo(Firmware)} splits the firmware * version by the delimiters ".", "-" and "_" and compares the different parts of the firmware version. As a result the * firmware version <i>2-0-1</i> is newer then firmware version <i>2.0.0</i> which again is newer than firmware version * <i>1-9_9.9_abc</i>. Consequently <i>2.0-0</i>, <i>2-0_0</i> and <i>2_0.0</i> represent the same firmware version. * Furthermore firmware version <i>xyz_1</i> is newer than firmware version <i>abc.2</i> which again is newer than * firmware version <i>2-0-1</i>. * </p> * A {@link Firmware} consists of various meta information like a version, a vendor or a description. Additionally * {@link FirmwareProvider}s can specify further meta information in form of properties (e.g. a factory reset of the * device is required afterwards) so that {@link FirmwareUpdateHandler}s can handle this information accordingly. * * @author Thomas Höfer - Initial contribution */ public final class Firmware implements Comparable<Firmware> { /** The key for the requires a factory reset property. */ public static final String PROPERTY_REQUIRES_FACTORY_RESET = "requiresFactoryReset"; private static final Logger logger = LoggerFactory.getLogger(Firmware.class); private final FirmwareUID uid; private final String vendor; private final String model; private final String description; private final String version; private final String prerequisiteVersion; private final String changelog; private final URL onlineChangelog; private final transient InputStream inputStream; private final String md5Hash; private final Map<String, String> properties; private transient byte[] bytes; private final Version internalVersion; private final Version internalPrerequisiteVersion; private Firmware(Builder builder) { this.uid = builder.uid; this.version = builder.uid.getFirmwareVersion(); this.vendor = builder.vendor; this.model = builder.model; this.description = builder.description; this.prerequisiteVersion = builder.prerequisiteVersion; this.changelog = builder.changelog; this.onlineChangelog = builder.onlineChangelog; this.inputStream = builder.inputStream; this.md5Hash = builder.md5Hash; this.properties = Collections .unmodifiableMap(builder.properties != null ? builder.properties : Collections.emptyMap()); this.internalVersion = new Version(this.version); this.internalPrerequisiteVersion = this.prerequisiteVersion != null ? new Version(this.prerequisiteVersion) : null; } /** * Returns the UID of the firmware. * * @return the UID of the firmware (not null) */ public FirmwareUID getUID() { return uid; } /** * Returns the vendor of the firmware. * * @return the vendor of the firmware (can be null) */ public String getVendor() { return vendor; } /** * Returns the model of the firmware. * * @return the model of the firmware (can be null) */ public String getModel() { return model; } /** * Returns the description of the firmware. * * @return the description of the firmware (can be null) */ public String getDescription() { return description; } /** * Returns the version of the firmware. * * @return the version of the firmware (not null) */ public String getVersion() { return version; } /** * Returns the prerequisite version of the firmware. * * @return the prerequisite version of the firmware (can be null) */ public String getPrerequisiteVersion() { return prerequisiteVersion; } /** * Returns the changelog of the firmware. * * @return the changelog of the firmware (can be null) */ public String getChangelog() { return changelog; } /** * Returns the URL to the online changelog of the firmware. * * @return the URL the an online changelog of the firmware (can be null) */ public URL getOnlineChangelog() { return onlineChangelog; } /** * Returns the input stream for the binary content of the firmware. * * @return the input stream for the binary content of the firmware (can be null) */ public InputStream getInputStream() { return inputStream; } /** * Returns the MD5 hash value of the firmware. * * @return the MD5 hash value of the firmware (can be null) */ public String getMd5Hash() { return md5Hash; } /** * Returns the binary content of the firmware using the firmware´s input stream. If the firmware provides a MD5 hash * value then this operation will also validate the MD5 checksum of the firmware. * * @return the binary content of the firmware (can be null) * * @throws IllegalStateException if the MD5 hash value of the firmware is invalid */ public synchronized byte[] getBytes() { if (inputStream == null) { return null; } if (bytes == null) { try { MessageDigest md = MessageDigest.getInstance("MD5"); try (DigestInputStream dis = new DigestInputStream(inputStream, md)) { bytes = IOUtils.toByteArray(dis); } catch (IOException ioEx) { logger.error(String.format("Cannot read firmware with UID %s.", uid), ioEx); return null; } byte[] digest = md.digest(); if (md5Hash != null && digest != null) { StringBuilder digestString = new StringBuilder(); for (byte b : digest) { digestString.append(String.format("%02x", b)); } if (!md5Hash.equals(digestString.toString())) { bytes = null; throw new IllegalStateException( String.format("Invalid MD5 checksum. Expected %s, but was %s.", md5Hash, digestString)); } } } catch (NoSuchAlgorithmException e) { logger.error("Cannot calculate MD5 checksum.", e); bytes = null; return null; } } return bytes; } /** * Returns the immutable properties of the firmware. * * @return the immutable properties of the firmware (not null) */ public Map<String, String> getProperties() { return properties; } /** * Returns true, if this firmware is a successor version of the given firmware version, otherwise false. If the * given firmware version is null, then this operation will return false. * * @param firmwareVersion the firmware version to be compared * * @return true, if this firmware is a successor version for the given firmware version, otherwise false */ public boolean isSuccessorVersion(String firmwareVersion) { if (firmwareVersion == null) { return false; } return internalVersion.compare(new Version(firmwareVersion)) > 0; } /** * Returns true, if this firmware is a valid prerequisite version of the given firmware version, otherwise false. * If this firmware does not have a prerequisite version or if the given firmware version is null, then this * operation will return false. * * @param firmwareVersion the firmware version to be checked if this firmware is valid prerequisite version of the * given firmware version * * @return true, if this firmware is valid prerequisite version of the given firmware version, otherwise false */ public boolean isPrerequisiteVersion(String firmwareVersion) { if (internalPrerequisiteVersion == null || firmwareVersion == null) { return false; } return new Version(firmwareVersion).compare(internalPrerequisiteVersion) >= 0; } @Override public int compareTo(Firmware firmware) { return -internalVersion.compare(new Version(firmware.getVersion())); } private static class Version { private static final int NO_INT = -1; private final String[] parts; private Version(String versionString) { this.parts = versionString.split("-|_|\\."); } private int compare(Version theVersion) { int max = Math.max(parts.length, theVersion.parts.length); for (int i = 0; i < max; i++) { String partA = i < parts.length ? parts[i] : null; String partB = i < theVersion.parts.length ? theVersion.parts[i] : null; Integer intA = partA != null && isInt(partA) ? Integer.parseInt(partA) : NO_INT; Integer intB = partB != null && isInt(partB) ? Integer.parseInt(partB) : NO_INT; if (intA != NO_INT && intB != NO_INT) { if (intA < intB) { return -1; } if (intA > intB) { return 1; } } else if (partA == null || partB == null) { if (partA == null) { return -1; } if (partB == null) { return 1; } } else { int result = partA.compareTo(partB); if (result != 0) { return result; } } } return 0; } private boolean isInt(String s) { return s.matches("^-?\\d+$"); } } /** * The builder to create a {@link Firmware}. */ public static final class Builder { private final FirmwareUID uid; private String vendor; private String model; private String description; private String prerequisiteVersion; private String changelog; private URL onlineChangelog; private transient InputStream inputStream; private String md5Hash; private Map<String, String> properties; /** * Creates a new builder. * * @param uid the UID of the firmware to be created (must not be null) * * @throws NullPointerException if given uid is null */ public Builder(FirmwareUID uid) { Preconditions.checkNotNull(uid, "Firmware UID must not be null."); this.uid = uid; } /** * Adds the vendor to the builder. * * @param vendor the vendor to be added to the builder * * @return the updated builder */ public Builder withVendor(String vendor) { this.vendor = vendor; return this; } /** * Adds the model to the builder. * * @param model the model to be added to the builder * * @return the updated builder */ public Builder withModel(String model) { this.model = model; return this; } /** * Adds the description to the builder. * * @param description the description to be added to the builder * * @return the updated builder */ public Builder withDescription(String description) { this.description = description; return this; } /** * Adds the prerequisite version to the builder. * * @param prerequisiteVersion the prerequisite version to be added to the builder * * @return the updated builder */ public Builder withPrerequisiteVersion(String prerequisiteVersion) { this.prerequisiteVersion = prerequisiteVersion; return this; } /** * Adds the changelog to the builder. * * @param changelog the changelog to be added to the builder * * @return the updated builder */ public Builder withChangelog(String changelog) { this.changelog = changelog; return this; } /** * Adds the online changelog to the builder. * * @param onlineChangelog the online changelog to be added to the builder * * @return the updated builder */ public Builder withOnlineChangelog(URL onlineChangelog) { this.onlineChangelog = onlineChangelog; return this; } /** * Adds the input stream for the binary content to the builder. * * @param inputStream the input stream for the binary content to be added to the builder * * @return the updated builder */ public Builder withInputStream(InputStream inputStream) { this.inputStream = inputStream; return this; } /** * Adds the properties to the builder. * * @param properties the properties to be added to the builder * * @return the updated builder */ public Builder withProperties(Map<String, String> properties) { this.properties = properties; return this; } /** * Adds the given md5 hash value to the builder. * * @param md5Hash the md5 hash value to be added to the builder * * @return the updated builder */ public Builder withMd5Hash(String md5Hash) { this.md5Hash = md5Hash; return this; } /** * Builds the firmware. * * @return the firmware instance based on this builder */ public Firmware build() { return new Firmware(this); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((changelog == null) ? 0 : changelog.hashCode()); result = prime * result + ((description == null) ? 0 : description.hashCode()); result = prime * result + ((md5Hash == null) ? 0 : md5Hash.hashCode()); result = prime * result + ((model == null) ? 0 : model.hashCode()); result = prime * result + ((onlineChangelog == null) ? 0 : onlineChangelog.hashCode()); result = prime * result + ((prerequisiteVersion == null) ? 0 : prerequisiteVersion.hashCode()); result = prime * result + ((uid == null) ? 0 : uid.hashCode()); result = prime * result + ((vendor == null) ? 0 : vendor.hashCode()); result = prime * result + ((version == null) ? 0 : version.hashCode()); result = prime * result + ((properties == null) ? 0 : properties.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Firmware other = (Firmware) obj; if (changelog == null) { if (other.changelog != null) { return false; } } else if (!changelog.equals(other.changelog)) { return false; } if (description == null) { if (other.description != null) { return false; } } else if (!description.equals(other.description)) { return false; } if (md5Hash == null) { if (other.md5Hash != null) { return false; } } else if (!md5Hash.equals(other.md5Hash)) { return false; } if (model == null) { if (other.model != null) { return false; } } else if (!model.equals(other.model)) { return false; } if (onlineChangelog == null) { if (other.onlineChangelog != null) { return false; } } else if (!onlineChangelog.equals(other.onlineChangelog)) { return false; } if (prerequisiteVersion == null) { if (other.prerequisiteVersion != null) { return false; } } else if (!prerequisiteVersion.equals(other.prerequisiteVersion)) { return false; } if (uid == null) { if (other.uid != null) { return false; } } else if (!uid.equals(other.uid)) { return false; } if (vendor == null) { if (other.vendor != null) { return false; } } else if (!vendor.equals(other.vendor)) { return false; } if (version == null) { if (other.version != null) { return false; } } else if (!version.equals(other.version)) { return false; } if (properties == null) { if (other.properties != null) { return false; } } else if (!properties.equals(other.properties)) { return false; } return true; } @Override public String toString() { return "Firmware [uid=" + uid + ", vendor=" + vendor + ", model=" + model + ", description=" + description + ", version=" + version + ", prerequisiteVersion=" + prerequisiteVersion + ", changelog=" + changelog + ", onlineChangelog=" + onlineChangelog + ", md5Hash=" + md5Hash + ", properties=" + properties + "]"; } }