Community Marketplace Add-on Service - initial contribution (#2405)
Signed-off-by: Yannick Schaus <github@schaus.net>pull/2487/head
parent
4d842f4ba3
commit
2663a3fc7e
|
@ -508,6 +508,12 @@
|
|||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.addon.marketplace</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="annotationpath" value="target/dependency"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="annotationpath" value="target/dependency"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.core.addon.marketplace</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
|
@ -0,0 +1,14 @@
|
|||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-core
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.reactor.bundles</artifactId>
|
||||
<version>3.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.core.addon.marketplace</artifactId>
|
||||
|
||||
<name>openHAB Core :: Bundles :: Community Marketplace Add-on Service</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.automation</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.config.core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.ui</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.addon.Addon;
|
||||
|
||||
/**
|
||||
* This interface can be implemented by services that want to register as handlers for specific marketplace add-on
|
||||
* content types and content types.
|
||||
* In a system there should always only be exactly one handler responsible for a given type+contentType
|
||||
* combination. If
|
||||
* multiple handers support it, it is undefined which one will be called.
|
||||
* This mechanism allows solutions to add support for specific formats (e.g. Karaf features) that are not supported by
|
||||
* openHAB out of the box.
|
||||
* It also allows to decide which add-on types are made available at all.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
* @author Yannick Schaus - refactoring
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface MarketplaceAddonHandler {
|
||||
|
||||
/**
|
||||
* Tells whether this handler supports a given add-on.
|
||||
*
|
||||
* @param addon the add-on in question
|
||||
* @return true, if the add-on is supported, false otherwise
|
||||
*/
|
||||
boolean supports(String type, String contentType);
|
||||
|
||||
/**
|
||||
* Tells whether a given add-on is currently installed.
|
||||
* Note: This method is only called, if the hander claimed support for the add-on before.
|
||||
*
|
||||
* @param id the id of the add-on in question
|
||||
* @return true, if the add-on is installed, false otherwise
|
||||
*/
|
||||
boolean isInstalled(String id);
|
||||
|
||||
/**
|
||||
* Installs a given add-on.
|
||||
* Note: This method is only called, if the hander claimed support for the add-on before.
|
||||
*
|
||||
* @param addon the add-on to install
|
||||
* @throws MarketplaceHandlerException if the installation failed for some reason
|
||||
*/
|
||||
void install(Addon addon) throws MarketplaceHandlerException;
|
||||
|
||||
/**
|
||||
* Uninstalls a given add-on.
|
||||
* Note: This method is only called, if the hander claimed support for the add-on before.
|
||||
*
|
||||
* @param addon the add-on to uninstall
|
||||
* @throws MarketplaceHandlerException if the uninstallation failed for some reason
|
||||
*/
|
||||
void uninstall(Addon addon) throws MarketplaceHandlerException;
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.osgi.framework.Bundle;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.BundleException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handle the management of bundles related to marketplace add-ons that resists OSGi cache cleanups.
|
||||
*
|
||||
* These operations cache incoming bundle files locally in a structure under the user data folder, and can make sure the
|
||||
* bundles are re-installed if they are present in the local cache but not installed in the OSGi framework.
|
||||
* They can be used by marketplace handler implementations dealing with OSGi bundles.
|
||||
*
|
||||
* @author Yannick Schaus - Initial contribution and API
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class MarketplaceBundleInstaller {
|
||||
private final Logger logger = LoggerFactory.getLogger(MarketplaceBundleInstaller.class);
|
||||
|
||||
private static final String BUNDLE_CACHE_PATH = OpenHAB.getUserDataFolder() + File.separator + "marketplace"
|
||||
+ File.separator + "bundles";
|
||||
|
||||
/**
|
||||
* Downloads a bundle file from a remote source and puts it in the local cache with the add-on ID.
|
||||
*
|
||||
* @param addonId the add-on ID
|
||||
* @param sourceUrl the (online) source where the .jar file can be found
|
||||
* @throws MarketplaceHandlerException
|
||||
*/
|
||||
protected void addBundleToCache(String addonId, URL sourceUrl) throws MarketplaceHandlerException {
|
||||
try {
|
||||
String fileName = new File(sourceUrl.toURI().getPath()).getName();
|
||||
File addonFile = new File(getAddonCacheDirectory(addonId), fileName);
|
||||
addonFile.getParentFile().mkdirs();
|
||||
|
||||
InputStream source = sourceUrl.openStream();
|
||||
Path outputPath = Path.of(addonFile.toURI());
|
||||
Files.copy(source, outputPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
throw new MarketplaceHandlerException("Cannot copy bundle to local cache: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a bundle from its ID by looking up in the local cache
|
||||
*
|
||||
* @param bundleContext the {@link BundleContext} to use to install the bundle
|
||||
* @param addonId the add-on ID
|
||||
* @throws MarketplaceHandlerException
|
||||
*/
|
||||
protected void installFromCache(BundleContext bundleContext, String addonId) throws MarketplaceHandlerException {
|
||||
File addonPath = getAddonCacheDirectory(addonId);
|
||||
if (addonPath.exists() && addonPath.isDirectory()) {
|
||||
File[] bundleFiles = addonPath.listFiles();
|
||||
if (bundleFiles.length != 1) {
|
||||
throw new MarketplaceHandlerException(
|
||||
"The local cache folder doesn't contain a single file: " + addonPath.toString());
|
||||
}
|
||||
|
||||
try (FileInputStream fileInputStream = new FileInputStream(bundleFiles[0])) {
|
||||
Bundle bundle = bundleContext.installBundle(addonId, fileInputStream);
|
||||
try {
|
||||
bundle.start();
|
||||
} catch (BundleException e) {
|
||||
logger.warn("The marketplace bundle was successfully installed but doesn't start: {}",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
} catch (IOException | BundleException e) {
|
||||
throw new MarketplaceHandlerException(
|
||||
"Cannot install bundle from marketplace cache: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a bundle associated to the given add-on ID is installed
|
||||
*
|
||||
* @param bundleContext the {@link BundleContext} to use to look up the bundle
|
||||
* @param addonId the add-on ID
|
||||
*/
|
||||
protected boolean isBundleInstalled(BundleContext bundleContext, String addonId) {
|
||||
return bundleContext.getBundle(addonId) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls a bundle associated to the given add-on ID. Also removes it from the local cache.
|
||||
*
|
||||
* @param bundleContext the {@link BundleContext} to use to look up the bundle
|
||||
* @param addonId the add-on ID
|
||||
*/
|
||||
protected void uninstallBundle(BundleContext bundleContext, String addonId) throws MarketplaceHandlerException {
|
||||
File addonPath = getAddonCacheDirectory(addonId);
|
||||
if (addonPath.exists() && addonPath.isDirectory()) {
|
||||
for (File bundleFile : addonPath.listFiles()) {
|
||||
bundleFile.delete();
|
||||
}
|
||||
}
|
||||
addonPath.delete();
|
||||
|
||||
if (isBundleInstalled(bundleContext, addonId)) {
|
||||
Bundle bundle = bundleContext.getBundle(addonId);
|
||||
try {
|
||||
bundle.stop();
|
||||
bundle.uninstall();
|
||||
} catch (BundleException e) {
|
||||
throw new MarketplaceHandlerException("Failed uninstalling bundle: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the local cache entries and re-installs bundles that are missing
|
||||
*
|
||||
* @param bundleContext the {@link BundleContext} to use to look up the bundles
|
||||
*/
|
||||
protected void ensureCachedBundlesAreInstalled(BundleContext bundleContext) {
|
||||
File addonPath = new File(BUNDLE_CACHE_PATH);
|
||||
if (addonPath.exists() && addonPath.isDirectory()) {
|
||||
for (File bundleFile : addonPath.listFiles()) {
|
||||
if (bundleFile.isDirectory()) {
|
||||
String addonId = "marketplace:" + bundleFile.getName();
|
||||
if (!isBundleInstalled(bundleContext, addonId)) {
|
||||
logger.info("Reinstalling missing marketplace bundle: {}", addonId);
|
||||
try {
|
||||
installFromCache(bundleContext, addonId);
|
||||
} catch (MarketplaceHandlerException e) {
|
||||
logger.warn("Failed reinstalling add-on from cache", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
bundleFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File getAddonCacheDirectory(String addonId) {
|
||||
return new File(BUNDLE_CACHE_PATH + File.separator + addonId.replace("marketplace:", ""));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This is an exception that can be thrown by {@link MarketplaceAddonHandler}s if some operation fails.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MarketplaceHandlerException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -5652014141471618161L;
|
||||
|
||||
/**
|
||||
* Main constructor
|
||||
*
|
||||
* @param message A message describing the issue
|
||||
*/
|
||||
public MarketplaceHandlerException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace.internal.automation;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.automation.dto.RuleTemplateDTO;
|
||||
import org.openhab.core.automation.dto.RuleTemplateDTOMapper;
|
||||
import org.openhab.core.automation.parser.Parser;
|
||||
import org.openhab.core.automation.parser.ParsingException;
|
||||
import org.openhab.core.automation.template.RuleTemplate;
|
||||
import org.openhab.core.automation.template.RuleTemplateProvider;
|
||||
import org.openhab.core.common.registry.AbstractManagedProvider;
|
||||
import org.openhab.core.storage.StorageService;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
|
||||
/**
|
||||
* This is a {@link RuleTemplateProvider}, which gets its content from the marketplace add-on service
|
||||
* and stores it through the OH storage service.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
* @author Yannick Schaus - refactoring
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { MarketplaceRuleTemplateProvider.class, RuleTemplateProvider.class })
|
||||
public class MarketplaceRuleTemplateProvider extends AbstractManagedProvider<RuleTemplate, String, RuleTemplateDTO>
|
||||
implements RuleTemplateProvider {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MarketplaceRuleTemplateProvider.class);
|
||||
|
||||
private final Parser<RuleTemplate> parser;
|
||||
ObjectMapper yamlMapper;
|
||||
|
||||
@Activate
|
||||
public MarketplaceRuleTemplateProvider(final @Reference StorageService storageService,
|
||||
final @Reference(target = "(&(format=json)(parser.type=parser.template))") Parser<RuleTemplate> parser) {
|
||||
super(storageService);
|
||||
this.parser = parser;
|
||||
this.yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||
yamlMapper.findAndRegisterModules();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable RuleTemplate getTemplate(String uid, @Nullable Locale locale) {
|
||||
return get(uid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<RuleTemplate> getTemplates(@Nullable Locale locale) {
|
||||
return getAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStorageName() {
|
||||
return "marketplace_ruletemplates";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String keyToString(String key) {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable RuleTemplate toElement(String key, RuleTemplateDTO persistableElement) {
|
||||
return RuleTemplateDTOMapper.map(persistableElement);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RuleTemplateDTO toPersistableElement(RuleTemplate element) {
|
||||
return RuleTemplateDTOMapper.map(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* This adds a new rule template to the persistent storage from its JSON representation.
|
||||
*
|
||||
* @param uid the UID to be used for the template
|
||||
* @param json the template content as a JSON string
|
||||
*
|
||||
* @throws ParsingException if the content cannot be parsed correctly
|
||||
*/
|
||||
public void addTemplateAsJSON(String uid, String json) throws ParsingException {
|
||||
try (InputStreamReader isr = new InputStreamReader(
|
||||
new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)))) {
|
||||
Set<RuleTemplate> templates = parser.parse(isr);
|
||||
if (templates.size() != 1) {
|
||||
throw new IllegalArgumentException("JSON must contain exactly one template!");
|
||||
} else {
|
||||
RuleTemplate entry = templates.iterator().next();
|
||||
// add a tag with the add-on ID to be able to identify the widget in the registry
|
||||
entry.getTags().add(uid);
|
||||
RuleTemplate template = new RuleTemplate(entry.getUID(), entry.getLabel(), entry.getDescription(),
|
||||
entry.getTags(), entry.getTriggers(), entry.getConditions(), entry.getActions(),
|
||||
entry.getConfigurationDescriptions(), entry.getVisibility());
|
||||
add(template);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Cannot close input stream.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This adds a new rule template to the persistent storage from its YAML representation.
|
||||
*
|
||||
* @param uid the UID to be used for the template
|
||||
* @param json the template content as a YAML string
|
||||
*
|
||||
* @throws ParsingException if the content cannot be parsed correctly
|
||||
*/
|
||||
public void addTemplateAsYAML(String uid, String yaml) throws ParsingException {
|
||||
try {
|
||||
RuleTemplateDTO dto = yamlMapper.readValue(yaml, RuleTemplateDTO.class);
|
||||
// add a tag with the add-on ID to be able to identify the widget in the registry
|
||||
dto.tags = new HashSet<@Nullable String>((dto.tags != null) ? dto.tags : new HashSet<String>());
|
||||
dto.tags.add(uid);
|
||||
RuleTemplate entry = RuleTemplateDTOMapper.map(dto);
|
||||
RuleTemplate template = new RuleTemplate(entry.getUID(), entry.getLabel(), entry.getDescription(),
|
||||
entry.getTags(), entry.getTriggers(), entry.getConditions(), entry.getActions(),
|
||||
entry.getConfigurationDescriptions(), entry.getVisibility());
|
||||
add(template);
|
||||
} catch (IOException e) {
|
||||
logger.error("Unable to parse YAML: {}", e.getMessage());
|
||||
throw new IllegalArgumentException("Unable to parse YAML");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace.internal.community;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.openhab.core.addon.Addon;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceBundleInstaller;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceHandlerException;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A {@link MarketplaceAddonHandler} implementation, which handles add-ons as jar files (specifically, OSGi
|
||||
* bundles) and installs them through the standard OSGi bundle installation mechanism.
|
||||
* The bundles installed this way have their location set to the add-on ID to identify them and determine their
|
||||
* installation status.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
* @author Yannick Schaus - refactoring
|
||||
*
|
||||
*/
|
||||
@Component(immediate = true)
|
||||
public class CommunityBundleAddonHandler extends MarketplaceBundleInstaller implements MarketplaceAddonHandler {
|
||||
|
||||
// add-on types supported by this handler
|
||||
private static final List<String> SUPPORTED_EXT_TYPES = Arrays.asList("binding");
|
||||
|
||||
private static final String BUNDLE_CONTENTTYPE = "application/vnd.openhab.bundle";
|
||||
|
||||
private static final String JAR_DOWNLOAD_URL_PROPERTY = "jar_download_url";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(CommunityBundleAddonHandler.class);
|
||||
|
||||
private BundleContext bundleContext;
|
||||
|
||||
@Activate
|
||||
protected void activate(BundleContext bundleContext, Map<String, Object> config) {
|
||||
this.bundleContext = bundleContext;
|
||||
ensureCachedBundlesAreInstalled(bundleContext);
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
protected void deactivate() {
|
||||
this.bundleContext = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String type, String contentType) {
|
||||
// we support only certain extension types, and only as pure OSGi bundles
|
||||
return SUPPORTED_EXT_TYPES.contains(type) && contentType.equals(BUNDLE_CONTENTTYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInstalled(String id) {
|
||||
return isBundleInstalled(bundleContext, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(Addon addon) throws MarketplaceHandlerException {
|
||||
URL sourceUrl;
|
||||
try {
|
||||
sourceUrl = new URL((String) addon.getProperties().get(JAR_DOWNLOAD_URL_PROPERTY));
|
||||
addBundleToCache(addon.getId(), sourceUrl);
|
||||
installFromCache(bundleContext, addon.getId());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new MarketplaceHandlerException("Malformed source URL: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstall(Addon addon) throws MarketplaceHandlerException {
|
||||
uninstallBundle(bundleContext, addon.getId());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,436 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace.internal.community;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.addon.Addon;
|
||||
import org.openhab.core.addon.AddonEventFactory;
|
||||
import org.openhab.core.addon.AddonService;
|
||||
import org.openhab.core.addon.AddonType;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceHandlerException;
|
||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponse;
|
||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponse.DiscoursePosterInfo;
|
||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponse.DiscourseTopicItem;
|
||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseCategoryResponse.DiscourseUser;
|
||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseTopicResponse;
|
||||
import org.openhab.core.addon.marketplace.internal.community.model.DiscourseTopicResponse.DiscoursePostLink;
|
||||
import org.openhab.core.config.core.ConfigurableService;
|
||||
import org.openhab.core.events.Event;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.osgi.framework.Constants;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Modified;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
import org.osgi.service.component.annotations.ReferencePolicy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* This class is a {@link AddonService} retrieving posts on community.openhab.org (Discourse).
|
||||
*
|
||||
* @author Yannick Schaus - Initial contribution
|
||||
*
|
||||
*/
|
||||
@Component(immediate = true, configurationPid = "org.openhab.marketplace", //
|
||||
property = Constants.SERVICE_PID + "=org.openhab.marketplace")
|
||||
@ConfigurableService(category = "system", label = "Community Marketplace", description_uri = CommunityMarketplaceAddonService.CONFIG_URI)
|
||||
public class CommunityMarketplaceAddonService implements AddonService {
|
||||
|
||||
// constants for the configuration properties
|
||||
static final String CONFIG_URI = "system:marketplace";
|
||||
static final String CONFIG_API_KEY = "apiKey";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(CommunityMarketplaceAddonService.class);
|
||||
|
||||
private static final String COMMUNITY_BASE_URL = "https://community.openhab.org";
|
||||
private static final String COMMUNITY_MARKETPLACE_URL = COMMUNITY_BASE_URL + "/c/marketplace/69/l/latest";
|
||||
private static final String COMMUNITY_TOPIC_URL = COMMUNITY_BASE_URL + "/t/";
|
||||
|
||||
private static final String ADDON_ID_PREFIX = "marketplace:";
|
||||
|
||||
private static final String JSON_CODE_MARKUP_START = "<pre><code class=\"lang-json\">";
|
||||
private static final String YAML_CODE_MARKUP_START = "<pre><code class=\"lang-yaml\">";
|
||||
private static final String CODE_MARKUP_END = "</code></pre>";
|
||||
|
||||
private HashMap<Integer, AddonType> types = new HashMap<Integer, AddonType>(3);
|
||||
private static final Integer BINDINGS_CATEGORY = 73;
|
||||
private static final Integer RULETEMPLATES_CATEGORY = 74;
|
||||
private static final Integer UIWIDGETS_CATEGORY = 75;
|
||||
|
||||
private static final String PUBLISHED_TAG = "published";
|
||||
|
||||
private HashMap<String, String> contentTypes = new HashMap<String, String>(3);
|
||||
private static final String BINDINGS_CONTENT_TYPE = "application/vnd.openhab.bundle";
|
||||
private static final String RULETEMPLATES_CONTENT_TYPE = "application/vnd.openhab.ruletemplate";
|
||||
private static final String UIWIDGETS_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=widget";
|
||||
|
||||
private final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").create();
|
||||
|
||||
private final Set<MarketplaceAddonHandler> addonHandlers = new HashSet<>();
|
||||
private EventPublisher eventPublisher;
|
||||
private String apiKey = null;
|
||||
|
||||
@Activate
|
||||
protected void activate(Map<String, Object> config) {
|
||||
types.put(BINDINGS_CATEGORY, new AddonType("binding", "Bindings"));
|
||||
types.put(RULETEMPLATES_CATEGORY, new AddonType("automation", "Automation"));
|
||||
types.put(UIWIDGETS_CATEGORY, new AddonType("ui", "User Interfaces"));
|
||||
contentTypes.put("binding", BINDINGS_CONTENT_TYPE);
|
||||
contentTypes.put("automation", RULETEMPLATES_CONTENT_TYPE);
|
||||
contentTypes.put("ui", UIWIDGETS_CONTENT_TYPE);
|
||||
modified(config);
|
||||
}
|
||||
|
||||
@Modified
|
||||
void modified(@Nullable Map<String, Object> config) {
|
||||
if (config != null) {
|
||||
this.apiKey = (String) config.get(CONFIG_API_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
|
||||
protected void addAddonHandler(MarketplaceAddonHandler handler) {
|
||||
this.addonHandlers.add(handler);
|
||||
}
|
||||
|
||||
protected void removeAddonHandler(MarketplaceAddonHandler handler) {
|
||||
this.addonHandlers.remove(handler);
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setEventPublisher(EventPublisher eventPublisher) {
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
protected void unsetEventPublisher(EventPublisher eventPublisher) {
|
||||
this.eventPublisher = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "marketplace";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Community Marketplace";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshSource() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Addon> getAddons(Locale locale) {
|
||||
try {
|
||||
List<DiscourseCategoryResponse> pages = new ArrayList<DiscourseCategoryResponse>();
|
||||
|
||||
URL url = new URL(COMMUNITY_MARKETPLACE_URL);
|
||||
int pageNb = 1;
|
||||
while (url != null) {
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.addRequestProperty("Accept", "application/json");
|
||||
if (this.apiKey != null) {
|
||||
connection.addRequestProperty("Api-Key", this.apiKey);
|
||||
}
|
||||
|
||||
try (Reader reader = new InputStreamReader(connection.getInputStream())) {
|
||||
DiscourseCategoryResponse parsed = gson.fromJson(reader, DiscourseCategoryResponse.class);
|
||||
pages.add(parsed);
|
||||
|
||||
if (parsed.topic_list.more_topics_url != null) {
|
||||
// Discourse URL for next page is wrong
|
||||
url = new URL(COMMUNITY_MARKETPLACE_URL + "?page=" + pageNb++);
|
||||
} else {
|
||||
url = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<DiscourseUser> users = pages.stream().flatMap(p -> Stream.of(p.users)).collect(Collectors.toList());
|
||||
return pages.stream().flatMap(p -> Stream.of(p.topic_list.topics))
|
||||
.filter(t -> Arrays.asList(t.tags).contains(PUBLISHED_TAG))
|
||||
.map(t -> convertTopicItemToAddon(t, users)).collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to retrieve marketplace add-ons", e);
|
||||
return new ArrayList<Addon>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Addon getAddon(String id, Locale locale) {
|
||||
URL url;
|
||||
try {
|
||||
url = new URL(String.format("%s%s", COMMUNITY_TOPIC_URL, id.replace(ADDON_ID_PREFIX, "")));
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.addRequestProperty("Accept", "application/json");
|
||||
if (this.apiKey != null) {
|
||||
connection.addRequestProperty("Api-Key", this.apiKey);
|
||||
}
|
||||
|
||||
try (Reader reader = new InputStreamReader(connection.getInputStream())) {
|
||||
DiscourseTopicResponse parsed = gson.fromJson(reader, DiscourseTopicResponse.class);
|
||||
return convertTopicToAddon(parsed);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AddonType> getTypes(Locale locale) {
|
||||
return new ArrayList<AddonType>(types.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(String id) {
|
||||
Addon addon = getAddon(id, null);
|
||||
for (MarketplaceAddonHandler handler : addonHandlers) {
|
||||
if (handler.supports(addon.getType(), addon.getContentType())) {
|
||||
if (!handler.isInstalled(addon.getId())) {
|
||||
try {
|
||||
handler.install(addon);
|
||||
postInstalledEvent(id);
|
||||
} catch (MarketplaceHandlerException e) {
|
||||
postFailureEvent(id, e.getMessage());
|
||||
}
|
||||
} else {
|
||||
postFailureEvent(id, "Add-on is already installed.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
postFailureEvent(id, "Add-on not known.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstall(String id) {
|
||||
Addon addon = getAddon(id, null);
|
||||
for (MarketplaceAddonHandler handler : addonHandlers) {
|
||||
if (handler.supports(addon.getType(), addon.getContentType())) {
|
||||
if (handler.isInstalled(addon.getId())) {
|
||||
try {
|
||||
handler.uninstall(addon);
|
||||
postUninstalledEvent(id);
|
||||
} catch (MarketplaceHandlerException e) {
|
||||
postFailureEvent(id, e.getMessage());
|
||||
}
|
||||
} else {
|
||||
postFailureEvent(id, "Add-on is not installed.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
postFailureEvent(id, "Add-on not known.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAddonId(URI addonURI) {
|
||||
if (addonURI.toString().startsWith(COMMUNITY_TOPIC_URL)) {
|
||||
return addonURI.toString().substring(0, addonURI.toString().indexOf("/", COMMUNITY_BASE_URL.length()));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a {@link DiscourseTopicItem} to a {@link Addon}
|
||||
*
|
||||
* @param topic the topic
|
||||
* @return the list item
|
||||
*/
|
||||
private Addon convertTopicItemToAddon(DiscourseTopicItem topic, List<DiscourseUser> users) {
|
||||
String id = ADDON_ID_PREFIX + topic.id.toString();
|
||||
AddonType addonType = types.get(topic.category_id);
|
||||
String type = (addonType != null) ? addonType.getId() : "";
|
||||
String contentType = (contentTypes.get(type) != null) ? contentTypes.get(type) : "";
|
||||
String version = "";
|
||||
String title = topic.title;
|
||||
String link = COMMUNITY_TOPIC_URL + topic.id.toString();
|
||||
int likeCount = topic.like_count;
|
||||
int views = topic.views;
|
||||
int postsCount = topic.posts_count;
|
||||
String[] tags = topic.tags;
|
||||
Date createdDate = topic.created_at;
|
||||
String author = "";
|
||||
boolean verifiedAuthor = false;
|
||||
for (DiscoursePosterInfo posterInfo : topic.posters) {
|
||||
if (posterInfo.description.contains("Original Poster")) {
|
||||
author = users.stream().filter(u -> u.id.equals(posterInfo.user_id)).findFirst().get().name;
|
||||
}
|
||||
}
|
||||
|
||||
HashMap<String, Object> properties = new HashMap<>(10);
|
||||
properties.put("created_at", createdDate);
|
||||
properties.put("like_count", likeCount);
|
||||
properties.put("views", views);
|
||||
properties.put("posts_count", postsCount);
|
||||
properties.put("tags", tags);
|
||||
|
||||
String description = "";
|
||||
String detailedDescription = "";
|
||||
|
||||
// try to use an handler to determine if the add-on is installed
|
||||
boolean installed = false;
|
||||
for (MarketplaceAddonHandler handler : addonHandlers) {
|
||||
if (handler.supports(type, contentType)) {
|
||||
if (handler.isInstalled(id)) {
|
||||
installed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String configDescriptionURI = "";
|
||||
String keywords = "";
|
||||
String countries = "";
|
||||
String connection = "";
|
||||
String backgroundColor = "";
|
||||
String imageLink = topic.image_url;
|
||||
Addon addon = new Addon(id, type, title, version, contentType, link, author, verifiedAuthor, installed,
|
||||
description, detailedDescription, configDescriptionURI, keywords, countries, connection,
|
||||
backgroundColor, imageLink, properties);
|
||||
return addon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes occurrences of XML entities found in the supplied content.
|
||||
*
|
||||
* @param content the content with potentially escaped entities
|
||||
* @return the unescaped content
|
||||
*/
|
||||
private String unescapeEntities(String content) {
|
||||
return content.replace(""", "\"").replace("&", "&").replace("'", "'").replace("<", "<")
|
||||
.replace(">", ">");
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a {@link DiscourseTopicResponse} to a {@link Addon}
|
||||
*
|
||||
* @param topic the topic
|
||||
* @return the list item
|
||||
*/
|
||||
private Addon convertTopicToAddon(DiscourseTopicResponse topic) {
|
||||
String id = ADDON_ID_PREFIX + topic.id.toString();
|
||||
AddonType addonType = types.get(topic.category_id);
|
||||
String type = (addonType != null) ? addonType.getId() : "";
|
||||
String contentType = contentTypes.get(type);
|
||||
String version = "";
|
||||
String title = topic.title;
|
||||
String link = COMMUNITY_TOPIC_URL + topic.id.toString();
|
||||
int likeCount = topic.like_count;
|
||||
int views = topic.views;
|
||||
int postsCount = topic.posts_count;
|
||||
String[] tags = topic.tags;
|
||||
Date createdDate = topic.post_stream.posts[0].created_at;
|
||||
Date updatedDate = topic.post_stream.posts[0].updated_at;
|
||||
Date lastPostedDate = topic.last_posted;
|
||||
String author = topic.post_stream.posts[0].display_username;
|
||||
boolean verifiedAuthor = false;
|
||||
|
||||
HashMap<String, Object> properties = new HashMap<>(10);
|
||||
properties.put("created_at", createdDate);
|
||||
properties.put("updated_at", updatedDate);
|
||||
properties.put("last_posted", lastPostedDate);
|
||||
properties.put("like_count", likeCount);
|
||||
properties.put("views", views);
|
||||
properties.put("posts_count", postsCount);
|
||||
properties.put("tags", tags);
|
||||
|
||||
String description = "";
|
||||
String detailedDescription = topic.post_stream.posts[0].cooked;
|
||||
|
||||
// try to extract contents or links
|
||||
if (topic.post_stream.posts[0].link_counts != null) {
|
||||
for (DiscoursePostLink postLink : topic.post_stream.posts[0].link_counts) {
|
||||
if (postLink.url.endsWith(".jar")) {
|
||||
properties.put("jar_download_url", postLink.url);
|
||||
}
|
||||
if (postLink.url.endsWith(".json")) {
|
||||
properties.put("json_download_url", postLink.url);
|
||||
}
|
||||
if (postLink.url.endsWith(".yaml")) {
|
||||
properties.put("yaml_download_url", postLink.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (detailedDescription.contains(JSON_CODE_MARKUP_START)) {
|
||||
String jsonContent = detailedDescription.substring(
|
||||
detailedDescription.indexOf(JSON_CODE_MARKUP_START) + JSON_CODE_MARKUP_START.length(),
|
||||
detailedDescription.indexOf(CODE_MARKUP_END, detailedDescription.indexOf(JSON_CODE_MARKUP_START)));
|
||||
properties.put("json_content", unescapeEntities(jsonContent));
|
||||
}
|
||||
if (detailedDescription.contains(YAML_CODE_MARKUP_START)) {
|
||||
String yamlContent = detailedDescription.substring(
|
||||
detailedDescription.indexOf(YAML_CODE_MARKUP_START) + YAML_CODE_MARKUP_START.length(),
|
||||
detailedDescription.indexOf(CODE_MARKUP_END, detailedDescription.indexOf(YAML_CODE_MARKUP_START)));
|
||||
properties.put("yaml_content", unescapeEntities(yamlContent));
|
||||
}
|
||||
|
||||
// try to use an handler to determine if the add-on is installed
|
||||
boolean installed = false;
|
||||
for (MarketplaceAddonHandler handler : addonHandlers) {
|
||||
if (handler.supports(type, (contentType != null) ? contentType : "")) {
|
||||
if (handler.isInstalled(id)) {
|
||||
installed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String configDescriptionURI = "";
|
||||
String keywords = "";
|
||||
String countries = "";
|
||||
String connection = "";
|
||||
String backgroundColor = "";
|
||||
Addon addon = new Addon(id, type, title, version, contentType, link, author, verifiedAuthor, installed,
|
||||
description, detailedDescription, configDescriptionURI, keywords, countries, connection,
|
||||
backgroundColor, null, properties);
|
||||
return addon;
|
||||
}
|
||||
|
||||
private void postInstalledEvent(String extensionId) {
|
||||
Event event = AddonEventFactory.createAddonInstalledEvent(extensionId);
|
||||
eventPublisher.post(event);
|
||||
}
|
||||
|
||||
private void postUninstalledEvent(String extensionId) {
|
||||
Event event = AddonEventFactory.createAddonUninstalledEvent(extensionId);
|
||||
eventPublisher.post(event);
|
||||
}
|
||||
|
||||
private void postFailureEvent(String extensionId, String msg) {
|
||||
Event event = AddonEventFactory.createAddonFailureEvent(extensionId, msg);
|
||||
eventPublisher.post(event);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace.internal.community;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.openhab.core.addon.Addon;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceHandlerException;
|
||||
import org.openhab.core.addon.marketplace.internal.automation.MarketplaceRuleTemplateProvider;
|
||||
import org.openhab.core.automation.template.RuleTemplateProvider;
|
||||
import org.openhab.core.storage.Storage;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A {@link MarketplaceAddonHandler} implementation, which handles rule templates as JSON files and installs
|
||||
* them by adding them to a {@link Storage}. The templates are then served from this storage through a dedicated
|
||||
* {@link RuleTemplateProvider}.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
* @author Yannick Schaus - refactoring
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class CommunityRuleTemplateAddonHandler implements MarketplaceAddonHandler {
|
||||
private static final String JSON_DOWNLOAD_URL_PROPERTY = "json_download_url";
|
||||
private static final String YAML_DOWNLOAD_URL_PROPERTY = "yaml_download_url";
|
||||
private static final String JSON_CONTENT_PROPERTY = "json_content";
|
||||
private static final String YAML_CONTENT_PROPERTY = "yaml_content";
|
||||
private static final String RULETEMPLATES_CONTENT_TYPE = "application/vnd.openhab.ruletemplate";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(CommunityRuleTemplateAddonHandler.class);
|
||||
|
||||
private MarketplaceRuleTemplateProvider marketplaceRuleTemplateProvider;
|
||||
|
||||
@Reference
|
||||
protected void setMarketplaceRuleTemplateProvider(MarketplaceRuleTemplateProvider marketplaceRuleTemplateProvider) {
|
||||
this.marketplaceRuleTemplateProvider = marketplaceRuleTemplateProvider;
|
||||
}
|
||||
|
||||
protected void unsetMarketplaceRuleTemplateProvider(
|
||||
MarketplaceRuleTemplateProvider marketplaceRuleTemplateProvider) {
|
||||
this.marketplaceRuleTemplateProvider = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String type, String contentType) {
|
||||
return "automation".equals(type) && RULETEMPLATES_CONTENT_TYPE.equals(contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInstalled(String id) {
|
||||
return marketplaceRuleTemplateProvider.getAll().stream().anyMatch(t -> t.getTags().contains(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(Addon addon) throws MarketplaceHandlerException {
|
||||
try {
|
||||
String template;
|
||||
if (addon.getProperties().containsKey(JSON_DOWNLOAD_URL_PROPERTY)) {
|
||||
template = getTemplateFromURL((String) addon.getProperties().get(JSON_DOWNLOAD_URL_PROPERTY));
|
||||
marketplaceRuleTemplateProvider.addTemplateAsJSON(addon.getId(), template);
|
||||
} else if (addon.getProperties().containsKey(YAML_DOWNLOAD_URL_PROPERTY)) {
|
||||
template = getTemplateFromURL((String) addon.getProperties().get(YAML_DOWNLOAD_URL_PROPERTY));
|
||||
marketplaceRuleTemplateProvider.addTemplateAsYAML(addon.getId(), template);
|
||||
} else if (addon.getProperties().containsKey(JSON_CONTENT_PROPERTY)) {
|
||||
template = (@NonNull String) addon.getProperties().get(JSON_CONTENT_PROPERTY);
|
||||
marketplaceRuleTemplateProvider.addTemplateAsJSON(addon.getId(), template);
|
||||
} else if (addon.getProperties().containsKey(YAML_CONTENT_PROPERTY)) {
|
||||
template = (@NonNull String) addon.getProperties().get(YAML_CONTENT_PROPERTY);
|
||||
marketplaceRuleTemplateProvider.addTemplateAsYAML(addon.getId(), template);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Rule template from marketplace cannot be downloaded: {}", e.getMessage());
|
||||
throw new MarketplaceHandlerException("Template cannot be downloaded.");
|
||||
} catch (Exception e) {
|
||||
logger.error("Rule template from marketplace is invalid: {}", e.getMessage());
|
||||
throw new MarketplaceHandlerException("Template is not valid.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstall(Addon addon) throws MarketplaceHandlerException {
|
||||
marketplaceRuleTemplateProvider.getAll().stream().filter(t -> t.getTags().contains(addon.getId()))
|
||||
.forEach(w -> {
|
||||
marketplaceRuleTemplateProvider.remove(w.getUID());
|
||||
});
|
||||
}
|
||||
|
||||
private String getTemplateFromURL(String urlString) throws IOException {
|
||||
URL u = new URL(urlString);
|
||||
try (InputStream in = u.openStream()) {
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace.internal.community;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.openhab.core.addon.Addon;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceAddonHandler;
|
||||
import org.openhab.core.addon.marketplace.MarketplaceHandlerException;
|
||||
import org.openhab.core.ui.components.RootUIComponent;
|
||||
import org.openhab.core.ui.components.UIComponentRegistry;
|
||||
import org.openhab.core.ui.components.UIComponentRegistryFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
|
||||
/**
|
||||
* A {@link MarketplaceAddonHandler} implementation, which handles UI widgets as YAML files and installs
|
||||
* them by adding them to the {@link UIComponentRegistry} for the ui:widget namespace.
|
||||
*
|
||||
* @author Yannick Schaus - Initial contribution and API
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class CommunityUIWidgetAddonHandler implements MarketplaceAddonHandler {
|
||||
private static final String YAML_DOWNLOAD_URL_PROPERTY = "yaml_download_url";
|
||||
private static final String YAML_CONTENT_PROPERTY = "yaml_content";
|
||||
private static final String UIWIDGETS_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=widget";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(CommunityUIWidgetAddonHandler.class);
|
||||
ObjectMapper yamlMapper;
|
||||
|
||||
private UIComponentRegistry widgetRegistry;
|
||||
|
||||
@Activate
|
||||
public CommunityUIWidgetAddonHandler(final @Reference UIComponentRegistryFactory uiComponentRegistryFactory) {
|
||||
this.widgetRegistry = uiComponentRegistryFactory.getRegistry("ui:widget");
|
||||
this.yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||
yamlMapper.findAndRegisterModules();
|
||||
this.yamlMapper.setDateFormat(new SimpleDateFormat("MMM d, yyyy, hh:mm:ss aa"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String type, String contentType) {
|
||||
return "ui".equals(type) && UIWIDGETS_CONTENT_TYPE.equals(contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInstalled(String id) {
|
||||
return widgetRegistry.getAll().stream().anyMatch(w -> w.hasTag(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(Addon addon) throws MarketplaceHandlerException {
|
||||
try {
|
||||
String widget;
|
||||
if (addon.getProperties().containsKey(YAML_DOWNLOAD_URL_PROPERTY)) {
|
||||
widget = getWidgetFromURL((String) addon.getProperties().get(YAML_DOWNLOAD_URL_PROPERTY));
|
||||
} else if (addon.getProperties().containsKey(YAML_CONTENT_PROPERTY)) {
|
||||
widget = (@NonNull String) addon.getProperties().get(YAML_CONTENT_PROPERTY);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Couldn't find the widget in the add-on entry");
|
||||
}
|
||||
addWidgetAsYAML(addon.getId(), widget);
|
||||
} catch (IOException e) {
|
||||
logger.error("Widget from marketplace cannot be downloaded: {}", e.getMessage());
|
||||
throw new MarketplaceHandlerException("Widget cannot be downloaded.");
|
||||
} catch (Exception e) {
|
||||
logger.error("Widget from marketplace is invalid: {}", e.getMessage());
|
||||
throw new MarketplaceHandlerException("Widget is not valid.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstall(Addon addon) throws MarketplaceHandlerException {
|
||||
widgetRegistry.getAll().stream().filter(w -> w.hasTag(addon.getId())).forEach(w -> {
|
||||
widgetRegistry.remove(w.getUID());
|
||||
});
|
||||
}
|
||||
|
||||
private String getWidgetFromURL(String urlString) throws IOException {
|
||||
URL u = new URL(urlString);
|
||||
try (InputStream in = u.openStream()) {
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private void addWidgetAsYAML(String id, String yaml) {
|
||||
try {
|
||||
RootUIComponent widget = yamlMapper.readValue(yaml, RootUIComponent.class);
|
||||
// add a tag with the add-on ID to be able to identify the widget in the registry
|
||||
widget.addTag(id);
|
||||
widgetRegistry.add(widget);
|
||||
} catch (IOException e) {
|
||||
logger.error("Unable to parse YAML: {}", e.getMessage());
|
||||
throw new IllegalArgumentException("Unable to parse YAML");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace.internal.community.model;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A DTO class mapped to the Discourse category topic list API.
|
||||
*
|
||||
* @author Yannick Schaus - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class DiscourseCategoryResponse {
|
||||
public DiscourseUser[] users;
|
||||
public DiscourseTopicList topic_list;
|
||||
|
||||
public class DiscourseUser {
|
||||
public Integer id;
|
||||
public String username;
|
||||
public String name;
|
||||
public String avatar_template;
|
||||
}
|
||||
|
||||
public class DiscourseTopicList {
|
||||
public String more_topics_url;
|
||||
public Integer per_page;
|
||||
public DiscourseTopicItem[] topics;
|
||||
}
|
||||
|
||||
public class DiscoursePosterInfo {
|
||||
public String extras;
|
||||
public String description;
|
||||
public Integer user_id;
|
||||
}
|
||||
|
||||
public class DiscourseTopicItem {
|
||||
public Integer id;
|
||||
public String title;
|
||||
public String slug;
|
||||
public String[] tags;
|
||||
public Integer posts_count;
|
||||
public String image_url;
|
||||
public Date created_at;
|
||||
public Integer like_count;
|
||||
public Integer views;
|
||||
public Integer category_id;
|
||||
public DiscoursePosterInfo[] posters;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.addon.marketplace.internal.community.model;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A DTO class mapped to the Discourse topic API.
|
||||
*
|
||||
* @author Yannick Schaus - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class DiscourseTopicResponse {
|
||||
public Integer id;
|
||||
|
||||
public DiscoursePostStream post_stream;
|
||||
|
||||
public String title;
|
||||
public Integer posts_count;
|
||||
|
||||
public Date created_at;
|
||||
public Date updated_at;
|
||||
public Date last_posted;
|
||||
|
||||
public Integer like_count;
|
||||
public Integer views;
|
||||
|
||||
public String[] tags;
|
||||
public Integer category_id;
|
||||
|
||||
public DiscourseTopicDetails details;
|
||||
|
||||
public class DiscoursePostAuthor {
|
||||
public Integer id;
|
||||
public String username;
|
||||
public String avatar_template;
|
||||
}
|
||||
|
||||
public class DiscoursePostLink {
|
||||
public String url;
|
||||
public Boolean internal;
|
||||
public Integer clicks;
|
||||
}
|
||||
|
||||
public class DiscoursePostStream {
|
||||
public DiscoursePost[] posts;
|
||||
}
|
||||
|
||||
public class DiscoursePost {
|
||||
public Integer id;
|
||||
|
||||
public String username;
|
||||
public String display_username;
|
||||
|
||||
public Date created_at;
|
||||
public Date updated_at;
|
||||
|
||||
public String cooked;
|
||||
|
||||
public DiscoursePostLink[] link_counts;
|
||||
}
|
||||
|
||||
public class DiscourseTopicDetails {
|
||||
public DiscoursePostAuthor created_by;
|
||||
public DiscoursePostAuthor last_poster;
|
||||
|
||||
public DiscoursePostLink[] links;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
|
||||
https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="system:marketplace">
|
||||
<parameter name="apiKey" type="text">
|
||||
<label>API Key for community.openhab.org</label>
|
||||
<description>Specify the API key to use on the community forum (for staff and curators - this allows for instance to
|
||||
see content which is not yet reviewed or otherwise hidden from the general public). Leave blank if you don't have
|
||||
one.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
|
@ -0,0 +1,4 @@
|
|||
system.config.marketplace.apiKey.label = API Key for community.openhab.org
|
||||
system.config.marketplace.defaultSource.description = Specify the API key to use on the community forum (for staff and curators - this allows for instance to see content which is not yet reviewed or otherwise hidden from the general public). Leave blank if you don't have one.
|
||||
|
||||
service.system.marketplace.label = Community Marketplace
|
|
@ -80,8 +80,8 @@ public class SampleAddonService implements AddonService {
|
|||
String description = createDescription();
|
||||
String imageLink = null;
|
||||
String backgroundColor = createRandomColor();
|
||||
Addon extension = new Addon(id, typeId, label, version, link, installed, description, backgroundColor,
|
||||
imageLink);
|
||||
Addon extension = new Addon(id, typeId, label, version, "example/vnd.openhab.addon", link, "John Doe",
|
||||
false, installed, description, backgroundColor, imageLink, null, null, null, null, null, null);
|
||||
extensions.put(extension.getId(), extension);
|
||||
}
|
||||
}
|
||||
|
@ -111,6 +111,20 @@ public class SampleAddonService implements AddonService {
|
|||
extensions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "sample";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Sample Add-on Service";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshSource() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(String id) {
|
||||
try {
|
||||
|
|
|
@ -14,6 +14,7 @@ package org.openhab.core.automation.template;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
@ -122,7 +123,7 @@ public class RuleTemplate implements Template {
|
|||
this.configDescriptions = configDescriptions == null ? Collections.emptyList()
|
||||
: Collections.unmodifiableList(configDescriptions);
|
||||
this.visibility = visibility == null ? Visibility.VISIBLE : visibility;
|
||||
this.tags = tags == null ? Collections.emptySet() : Collections.unmodifiableSet(tags);
|
||||
this.tags = tags == null ? new HashSet<>() : new HashSet<>(tags);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,6 +29,9 @@ public class ConfigDescriptionDTO {
|
|||
|
||||
public List<ConfigDescriptionParameterGroupDTO> parameterGroups;
|
||||
|
||||
public ConfigDescriptionDTO() {
|
||||
}
|
||||
|
||||
public ConfigDescriptionDTO(String uri, List<ConfigDescriptionParameterDTO> parameters,
|
||||
List<ConfigDescriptionParameterGroupDTO> parameterGroups) {
|
||||
this.uri = uri;
|
||||
|
|
|
@ -29,6 +29,7 @@ import javax.ws.rs.POST;
|
|||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
@ -37,6 +38,7 @@ import javax.ws.rs.core.UriInfo;
|
|||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.core.addon.Addon;
|
||||
import org.openhab.core.addon.AddonEventFactory;
|
||||
import org.openhab.core.addon.AddonService;
|
||||
|
@ -79,6 +81,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||
* @author Franck Dechavanne - Added DTOs to ApiResponses
|
||||
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
|
||||
* @author Wouter Born - Migrated to OpenAPI annotations
|
||||
* @author Yannick Schaus - Add service-related parameters & operations
|
||||
*/
|
||||
@Component
|
||||
@JaxrsResource
|
||||
|
@ -96,6 +99,8 @@ public class AddonResource implements RESTResource {
|
|||
|
||||
public static final String PATH_ADDONS = "addons";
|
||||
|
||||
public static final String DEFAULT_ADDON_SERVICE = "karaf";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AddonResource.class);
|
||||
private final Set<AddonService> addonServices = new CopyOnWriteArraySet<>();
|
||||
private final EventPublisher eventPublisher;
|
||||
|
@ -121,25 +126,61 @@ public class AddonResource implements RESTResource {
|
|||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(operationId = "getAddons", summary = "Get all add-ons.", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Addon.class)))) })
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Addon.class)))),
|
||||
@ApiResponse(responseCode = "404", description = "Service not found") })
|
||||
public Response getAddon(
|
||||
@HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language,
|
||||
@QueryParam("serviceId") @Parameter(description = "service ID") @Nullable String serviceId) {
|
||||
logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath());
|
||||
Locale locale = localeService.getLocale(language);
|
||||
if (serviceId == "all") {
|
||||
return Response.ok(new Stream2JSONInputStream(getAllAddons(locale))).build();
|
||||
} else {
|
||||
AddonService addonService = (serviceId != null) ? getServiceById(serviceId) : getDefaultService();
|
||||
if (addonService == null) {
|
||||
return Response.status(HttpStatus.NOT_FOUND_404).build();
|
||||
}
|
||||
return Response.ok(new Stream2JSONInputStream(addonService.getAddons(locale).stream())).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/services")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(operationId = "getAddonTypes", summary = "Get all add-on types.", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = AddonType.class)))) })
|
||||
public Response getServices(
|
||||
@HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language) {
|
||||
logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath());
|
||||
Locale locale = localeService.getLocale(language);
|
||||
return Response.ok(new Stream2JSONInputStream(getAllAddons(locale))).build();
|
||||
Stream<AddonServiceDTO> addonTypeStream = addonServices.stream().map(s -> convertToAddonServiceDTO(s, locale));
|
||||
return Response.ok(new Stream2JSONInputStream(addonTypeStream)).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/types")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(operationId = "getAddonTypes", summary = "Get all add-on types.", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = AddonType.class)))) })
|
||||
@Operation(operationId = "getAddonServices", summary = "Get add-on services.", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = AddonType.class)))),
|
||||
@ApiResponse(responseCode = "404", description = "Service not found") })
|
||||
public Response getTypes(
|
||||
@HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language) {
|
||||
@HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language,
|
||||
@QueryParam("serviceId") @Parameter(description = "service ID") @Nullable String serviceId) {
|
||||
logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath());
|
||||
Locale locale = localeService.getLocale(language);
|
||||
Stream<AddonType> addonTypeStream = getAllAddonTypes(locale).stream().distinct();
|
||||
return Response.ok(new Stream2JSONInputStream(addonTypeStream)).build();
|
||||
if (serviceId != null) {
|
||||
@Nullable
|
||||
AddonService service = getServiceById(serviceId);
|
||||
if (service != null) {
|
||||
Stream<AddonType> addonTypeStream = getAddonTypesForService(service, locale).stream().distinct();
|
||||
return Response.ok(new Stream2JSONInputStream(addonTypeStream)).build();
|
||||
} else {
|
||||
return Response.status(HttpStatus.NOT_FOUND_404).build();
|
||||
}
|
||||
} else {
|
||||
Stream<AddonType> addonTypeStream = getAllAddonTypes(locale).stream().distinct();
|
||||
return Response.ok(new Stream2JSONInputStream(addonTypeStream)).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -150,26 +191,35 @@ public class AddonResource implements RESTResource {
|
|||
@ApiResponse(responseCode = "404", description = "Not found") })
|
||||
public Response getById(
|
||||
@HeaderParam("Accept-Language") @Parameter(description = "language") @Nullable String language,
|
||||
@PathParam("addonId") @Parameter(description = "addon ID") String addonId) {
|
||||
@PathParam("addonId") @Parameter(description = "addon ID") String addonId,
|
||||
@QueryParam("serviceId") @Parameter(description = "service ID") @Nullable String serviceId) {
|
||||
logger.debug("Received HTTP GET request at '{}'.", uriInfo.getPath());
|
||||
Locale locale = localeService.getLocale(language);
|
||||
AddonService addonService = getAddonService(addonId);
|
||||
AddonService addonService = (serviceId != null) ? getServiceById(serviceId) : getDefaultService();
|
||||
if (addonService == null) {
|
||||
return Response.status(HttpStatus.NOT_FOUND_404).build();
|
||||
}
|
||||
Addon responseObject = addonService.getAddon(addonId, locale);
|
||||
if (responseObject != null) {
|
||||
return Response.ok(responseObject).build();
|
||||
}
|
||||
|
||||
return Response.status(404).build();
|
||||
return Response.status(HttpStatus.NOT_FOUND_404).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{addonId: [a-zA-Z_0-9-:]+}/install")
|
||||
@Operation(operationId = "installAddonById", summary = "Installs the add-on with the given ID.", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK") })
|
||||
public Response installAddon(final @PathParam("addonId") @Parameter(description = "addon ID") String addonId) {
|
||||
@ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(responseCode = "404", description = "Not found") })
|
||||
public Response installAddon(final @PathParam("addonId") @Parameter(description = "addon ID") String addonId,
|
||||
@QueryParam("serviceId") @Parameter(description = "service ID") @Nullable String serviceId) {
|
||||
AddonService addonService = (serviceId != null) ? getServiceById(serviceId) : getDefaultService();
|
||||
if (addonService == null) {
|
||||
return Response.status(HttpStatus.NOT_FOUND_404).build();
|
||||
}
|
||||
ThreadPoolManager.getPool(THREAD_POOL_NAME).submit(() -> {
|
||||
try {
|
||||
AddonService addonService = getAddonService(addonId);
|
||||
addonService.install(addonId);
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception while installing add-on: {}", e.getMessage());
|
||||
|
@ -189,7 +239,7 @@ public class AddonResource implements RESTResource {
|
|||
try {
|
||||
URI addonURI = new URI(url);
|
||||
String addonId = getAddonId(addonURI);
|
||||
installAddon(addonId);
|
||||
installAddon(addonId, getAddonServiceForAddonId(addonURI));
|
||||
} catch (URISyntaxException | IllegalArgumentException e) {
|
||||
logger.error("Exception while parsing the addon URL '{}': {}", url, e.getMessage());
|
||||
return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "The given URL is malformed or not valid.");
|
||||
|
@ -201,11 +251,16 @@ public class AddonResource implements RESTResource {
|
|||
@POST
|
||||
@Path("/{addonId: [a-zA-Z_0-9-:]+}/uninstall")
|
||||
@Operation(operationId = "uninstallAddon", summary = "Uninstalls the add-on with the given ID.", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK") })
|
||||
public Response uninstallAddon(final @PathParam("addonId") @Parameter(description = "addon ID") String addonId) {
|
||||
@ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(responseCode = "404", description = "Not found") })
|
||||
public Response uninstallAddon(final @PathParam("addonId") @Parameter(description = "addon ID") String addonId,
|
||||
@QueryParam("serviceId") @Parameter(description = "service ID") @Nullable String serviceId) {
|
||||
AddonService addonService = (serviceId != null) ? getServiceById(serviceId) : getDefaultService();
|
||||
if (addonService == null) {
|
||||
return Response.status(HttpStatus.NOT_FOUND_404).build();
|
||||
}
|
||||
ThreadPoolManager.getPool(THREAD_POOL_NAME).submit(() -> {
|
||||
try {
|
||||
AddonService addonService = getAddonService(addonId);
|
||||
addonService.uninstall(addonId);
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception while uninstalling add-on: {}", e.getMessage());
|
||||
|
@ -220,6 +275,15 @@ public class AddonResource implements RESTResource {
|
|||
eventPublisher.post(event);
|
||||
}
|
||||
|
||||
private AddonService getDefaultService() {
|
||||
for (AddonService addonService : addonServices) {
|
||||
if (addonService.getId().equals(DEFAULT_ADDON_SERVICE)) {
|
||||
return addonService;
|
||||
}
|
||||
}
|
||||
return addonServices.iterator().next();
|
||||
}
|
||||
|
||||
private Stream<Addon> getAllAddons(Locale locale) {
|
||||
return addonServices.stream().map(s -> s.getAddons(locale)).flatMap(l -> l.stream());
|
||||
}
|
||||
|
@ -239,15 +303,26 @@ public class AddonResource implements RESTResource {
|
|||
return ret;
|
||||
}
|
||||
|
||||
private AddonService getAddonService(final String addonId) {
|
||||
private Set<AddonType> getAddonTypesForService(AddonService addonService, Locale locale) {
|
||||
final Collator coll = Collator.getInstance(locale);
|
||||
coll.setStrength(Collator.PRIMARY);
|
||||
Set<AddonType> ret = new TreeSet<>(new Comparator<AddonType>() {
|
||||
@Override
|
||||
public int compare(AddonType o1, AddonType o2) {
|
||||
return coll.compare(o1.getLabel(), o2.getLabel());
|
||||
}
|
||||
});
|
||||
ret.addAll(addonService.getTypes(locale));
|
||||
return ret;
|
||||
}
|
||||
|
||||
private @Nullable AddonService getServiceById(final String serviceId) {
|
||||
for (AddonService addonService : addonServices) {
|
||||
for (Addon addon : addonService.getAddons(Locale.getDefault())) {
|
||||
if (addonId.equals(addon.getId())) {
|
||||
return addonService;
|
||||
}
|
||||
if (addonService.getId().equals(serviceId)) {
|
||||
return addonService;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No add-on service registered for " + addonId);
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getAddonId(URI addonURI) {
|
||||
|
@ -260,4 +335,20 @@ public class AddonResource implements RESTResource {
|
|||
|
||||
throw new IllegalArgumentException("No add-on service registered for URI " + addonURI);
|
||||
}
|
||||
|
||||
private String getAddonServiceForAddonId(URI addonURI) {
|
||||
for (AddonService addonService : addonServices) {
|
||||
String addonId = addonService.getAddonId(addonURI);
|
||||
if (addonId != null && !addonId.isBlank()) {
|
||||
return addonService.getId();
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("No add-on service registered for URI " + addonURI);
|
||||
}
|
||||
|
||||
private AddonServiceDTO convertToAddonServiceDTO(AddonService addonService, Locale locale) {
|
||||
return new AddonServiceDTO(addonService.getId(), addonService.getName(),
|
||||
getAddonTypesForService(addonService, locale));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.io.rest.core.internal.addons;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.addon.AddonType;
|
||||
|
||||
/**
|
||||
* A DTO representing an add-on service.
|
||||
*
|
||||
* @author Yannick Schaus - initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AddonServiceDTO {
|
||||
String id;
|
||||
String name;
|
||||
Set<AddonType> addonTypes;
|
||||
|
||||
public AddonServiceDTO(String id, String name, Set<AddonType> addonTypes) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.addonTypes = addonTypes;
|
||||
}
|
||||
}
|
|
@ -39,6 +39,8 @@ import org.slf4j.LoggerFactory;
|
|||
*/
|
||||
@Component(name = "org.openhab.core.karafaddons")
|
||||
public class KarafAddonService implements AddonService {
|
||||
private static final String ADDONS_CONTENTTYPE = "application/vnd.openhab.feature;type=karaf";
|
||||
private static final String ADDONS_AUTHOR = "openHAB";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(KarafAddonService.class);
|
||||
|
||||
|
@ -63,6 +65,20 @@ public class KarafAddonService implements AddonService {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "karaf";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "openHAB Distribution";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshSource() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Addon> getAddons(Locale locale) {
|
||||
List<Addon> addons = new LinkedList<>();
|
||||
|
@ -133,7 +149,7 @@ public class KarafAddonService implements AddonService {
|
|||
break;
|
||||
}
|
||||
boolean installed = featuresService.isInstalled(feature);
|
||||
return new Addon(extId, type, label, version, link, installed);
|
||||
return new Addon(extId, type, label, version, ADDONS_CONTENTTYPE, link, ADDONS_AUTHOR, true, installed);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -43,10 +43,17 @@ public class RootUIComponent extends UIComponent implements Identifiable<String>
|
|||
@Nullable
|
||||
Date timestamp;
|
||||
|
||||
/**
|
||||
* Empty constructor for deserialization.
|
||||
*/
|
||||
public RootUIComponent() {
|
||||
this("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a root component.
|
||||
*
|
||||
* @param name the name of the UI component to render the card on client frontends, ie. "HbCard"
|
||||
* @param name the name of the UI component to render on client frontends, ie. "oh-block"
|
||||
*/
|
||||
public RootUIComponent(String name) {
|
||||
super(name);
|
||||
|
@ -58,7 +65,7 @@ public class RootUIComponent extends UIComponent implements Identifiable<String>
|
|||
* Constructs a root component with a specific UID.
|
||||
*
|
||||
* @param uid the UID of the new card
|
||||
* @param name the name of the UI component to render the card on client frontends, ie. "HbCard"
|
||||
* @param name the name of the UI component to render on client frontends, ie. "oh-block"
|
||||
*/
|
||||
public RootUIComponent(String uid, String name) {
|
||||
super(name);
|
||||
|
|
|
@ -37,6 +37,12 @@ public class UIComponent {
|
|||
|
||||
Map<String, List<UIComponent>> slots = null;
|
||||
|
||||
/**
|
||||
* Empty constructor for deserialization.
|
||||
*/
|
||||
public UIComponent() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a component by its type name - component names are not arbitrary, they are defined by the target
|
||||
* frontend.
|
||||
|
@ -58,6 +64,24 @@ public class UIComponent {
|
|||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the type of the component.
|
||||
*
|
||||
* @return the component type
|
||||
*/
|
||||
public String getComponent() {
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type of the component.
|
||||
*
|
||||
* @return the component type
|
||||
*/
|
||||
public void setComponent(String component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the configuration parameters of the component
|
||||
*
|
||||
|
@ -67,6 +91,15 @@ public class UIComponent {
|
|||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all the configuration parameters of the component
|
||||
*
|
||||
* @param config the map of configuration parameters
|
||||
*/
|
||||
public void setConfig(Map<String, Object> config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new configuration parameter to the component
|
||||
*
|
||||
|
@ -78,7 +111,7 @@ public class UIComponent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns all the slots of the components including their sub-components
|
||||
* Returns all the slots of the component including their sub-components
|
||||
*
|
||||
* @return the slots and their sub-components
|
||||
*/
|
||||
|
@ -86,6 +119,15 @@ public class UIComponent {
|
|||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all the slots of the component
|
||||
*
|
||||
* @param slots the slots and their sub-components
|
||||
*/
|
||||
public void setSlots(Map<String, List<UIComponent>> slots) {
|
||||
this.slots = slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new empty slot to the component
|
||||
*
|
||||
|
|
|
@ -12,22 +12,34 @@
|
|||
*/
|
||||
package org.openhab.core.addon;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This class defines an add-on.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
* @author Yannick Schaus - Add fields
|
||||
*/
|
||||
public class Addon {
|
||||
|
||||
private final String id;
|
||||
private final String label;
|
||||
private final String version;
|
||||
private final String contentType;
|
||||
private final String link;
|
||||
private final String author;
|
||||
private boolean verifiedAuthor;
|
||||
private boolean installed;
|
||||
private final String type;
|
||||
private final String description;
|
||||
private final String detailedDescription;
|
||||
private final String configDescriptionURI;
|
||||
private final String keywords;
|
||||
private final String countries;
|
||||
private final String connection;
|
||||
private final String backgroundColor;
|
||||
private final String imageLink;
|
||||
private final Map<String, Object> properties;
|
||||
|
||||
/**
|
||||
* Creates a new Addon instance
|
||||
|
@ -36,11 +48,16 @@ public class Addon {
|
|||
* @param type the type id of the add-on
|
||||
* @param label the label of the add-on
|
||||
* @param version the version of the add-on
|
||||
* @param contentType the content type of the add-on
|
||||
* @param link the link to find more information about the add-on (can be null)
|
||||
* @param author the author of the add-on
|
||||
* @param verifiedAuthor true, if the author is verified
|
||||
* @param installed true, if the add-on is installed, false otherwise
|
||||
*/
|
||||
public Addon(String id, String type, String label, String version, String link, boolean installed) {
|
||||
this(id, type, label, version, link, installed, null, null, null);
|
||||
public Addon(String id, String type, String label, String version, String contentType, String link, String author,
|
||||
boolean verifiedAuthor, boolean installed) {
|
||||
this(id, type, label, version, contentType, link, author, verifiedAuthor, installed, null, null, null, null,
|
||||
null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,23 +67,37 @@ public class Addon {
|
|||
* @param type the type id of the add-on
|
||||
* @param label the label of the add-on
|
||||
* @param version the version of the add-on
|
||||
* @param contentType the content type of the add-on
|
||||
* @param description the detailed description of the add-on (may be null)
|
||||
* @param backgroundColor for displaying the add-on (may be null)
|
||||
* @param link the link to find more information about the add-on (may be null)
|
||||
* @param author the author of the add-on
|
||||
* @param verifiedAuthor true, if the author is verified
|
||||
* @param imageLink the link to an image (png/svg) (may be null)
|
||||
* @param installed true, if the add-on is installed, false otherwise
|
||||
*/
|
||||
public Addon(String id, String type, String label, String version, String link, boolean installed,
|
||||
String description, String backgroundColor, String imageLink) {
|
||||
public Addon(String id, String type, String label, String version, String contentType, String link, String author,
|
||||
boolean verifiedAuthor, boolean installed, String description, String detailedDescription,
|
||||
String configDescriptionURI, String keywords, String countries, String connection, String backgroundColor,
|
||||
String imageLink, Map<String, Object> properties) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.version = version;
|
||||
this.contentType = contentType;
|
||||
this.description = description;
|
||||
this.detailedDescription = detailedDescription;
|
||||
this.configDescriptionURI = configDescriptionURI;
|
||||
this.keywords = keywords;
|
||||
this.countries = countries;
|
||||
this.connection = connection;
|
||||
this.backgroundColor = backgroundColor;
|
||||
this.link = link;
|
||||
this.imageLink = imageLink;
|
||||
this.author = author;
|
||||
this.verifiedAuthor = verifiedAuthor;
|
||||
this.installed = installed;
|
||||
this.type = type;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,6 +128,20 @@ public class Addon {
|
|||
return link;
|
||||
}
|
||||
|
||||
/**
|
||||
* The author of the add-on
|
||||
*/
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the add-on author is verified or not
|
||||
*/
|
||||
public boolean isVerifiedAuthor() {
|
||||
return verifiedAuthor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The version of the add-on
|
||||
*/
|
||||
|
@ -104,6 +149,62 @@ public class Addon {
|
|||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* The content type of the add-on
|
||||
*/
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The description of the add-on
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* The detailed description of the add-on
|
||||
*/
|
||||
public String getDetailedDescription() {
|
||||
return detailedDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URI to the configuration description for this add-on
|
||||
*/
|
||||
public String getConfigDescriptionURI() {
|
||||
return configDescriptionURI;
|
||||
}
|
||||
|
||||
/**
|
||||
* The keywords for this add-on
|
||||
*/
|
||||
public String getKeywords() {
|
||||
return keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* A comma-separated list of ISO 3166 codes relevant to this add-on
|
||||
*/
|
||||
public String getCountries() {
|
||||
return countries;
|
||||
}
|
||||
|
||||
/**
|
||||
* A string describing the type of connection (local or cloud, push or pull...) this add-on uses, if applicable.
|
||||
*/
|
||||
public String getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of additional properties relative to this add-on
|
||||
*/
|
||||
public Map<String, Object> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* true, if the add-on is installed, false otherwise
|
||||
*/
|
||||
|
@ -118,13 +219,6 @@ public class Addon {
|
|||
this.installed = installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* The description of the add-on
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* The background color for rendering the add-on
|
||||
*/
|
||||
|
|
|
@ -22,11 +22,34 @@ import java.util.Locale;
|
|||
* The REST API offers an uri that exposes this functionality.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
* @author Yannick Schaus - Add id, name and refreshSource
|
||||
*/
|
||||
public interface AddonService {
|
||||
|
||||
/**
|
||||
* Retrieves all add-ons
|
||||
* Returns the ID of the service.
|
||||
*
|
||||
* @return the service identifier
|
||||
*/
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Returns the name of the service.
|
||||
*
|
||||
* @return the service name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Refreshes the source used for providing the add-ons.
|
||||
*
|
||||
* This can be called before getAddons to ensure the add-on information is up-to-date; otherwise they might be
|
||||
* retrieved from a cache.
|
||||
*/
|
||||
void refreshSource();
|
||||
|
||||
/**
|
||||
* Retrieves all add-ons.
|
||||
*
|
||||
* It is expected that this method is rather cheap to call and will return quickly, i.e. some caching should be
|
||||
* implemented if required.
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<name>openHAB Core :: Bundles</name>
|
||||
|
||||
<modules>
|
||||
<module>org.openhab.core.addon.marketplace</module>
|
||||
<module>org.openhab.core.auth.jaas</module>
|
||||
<module>org.openhab.core.auth.oauth2client</module>
|
||||
<module>org.openhab.core.automation</module>
|
||||
|
|
|
@ -64,6 +64,14 @@
|
|||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.sse/${project.version}</bundle>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab-core-addon-marketplace" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.addon.marketplace/${project.version}</bundle>
|
||||
<requirement>openhab.tp;filter:="(feature=jackson)"</requirement>
|
||||
<feature dependency="true">openhab.tp-jackson</feature>
|
||||
<feature dependency="true">openhab-core-ui</feature>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab-core-auth-jaas" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.auth.jaas/${project.version}</bundle>
|
||||
|
@ -361,6 +369,7 @@
|
|||
|
||||
<feature name="openhab-runtime-base" description="openHAB Runtime Base" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<feature>openhab-core-addon-marketplace</feature>
|
||||
<feature>openhab-core-auth-jaas</feature>
|
||||
<feature>openhab-core-automation-rest</feature>
|
||||
<feature>openhab-core-automation-module-script</feature>
|
||||
|
|
Loading…
Reference in New Issue