[marketplace] Add add-on handler for block libraries (#2598)

Related to https://github.com/openhab/openhab-webui/pull/1225.

This add-on handler supports installing block libraries in the
UIComponentRegistry, `ui:blocks` namespace.

It is very similar to the CommunityUIWidgetAddonHandler, as the format
is the same, so are the features and implementation, the only
differences being:

- Supported content type: `application/vnd.openhab.uicomponent;type=blocks`
  (vs. `application/vnd.openhab.uicomponent;type=widget`)
- Discourse category nb.: 76 (vs. 75)
- Inferred Add-on type: "automation" (vs. "ui")

Signed-off-by: Yannick Schaus <github@schaus.net>
This commit is contained in:
Yannick Schaus
2021-12-10 23:35:25 +01:00
committed by GitHub
parent 0028a3c792
commit f641d1d6ec
2 changed files with 127 additions and 0 deletions
@@ -0,0 +1,121 @@
/**
* 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.NonNullByDefault;
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 block libraries as YAML files and installs
* them by adding them to the {@link UIComponentRegistry} for the ui:blocks namespace.
*
* @author Yannick Schaus - Initial contribution and API
*
*/
@Component(immediate = true)
@NonNullByDefault
public class CommunityBlockLibaryAddonHandler 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 BLOCKLIBRARIES_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=blocks";
private final Logger logger = LoggerFactory.getLogger(CommunityBlockLibaryAddonHandler.class);
ObjectMapper yamlMapper;
private UIComponentRegistry blocksRegistry;
@Activate
public CommunityBlockLibaryAddonHandler(final @Reference UIComponentRegistryFactory uiComponentRegistryFactory) {
this.blocksRegistry = uiComponentRegistryFactory.getRegistry("ui:blocks");
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 "automation".equals(type) && BLOCKLIBRARIES_CONTENT_TYPE.equals(contentType);
}
@Override
public boolean isInstalled(String id) {
return blocksRegistry.getAll().stream().anyMatch(w -> w.hasTag(id));
}
@Override
public void install(Addon addon) throws MarketplaceHandlerException {
try {
String yamlDownloadUrl = (String) addon.getProperties().get(YAML_DOWNLOAD_URL_PROPERTY);
String yamlContent = (String) addon.getProperties().get(YAML_CONTENT_PROPERTY);
if (yamlDownloadUrl != null) {
addWidgetAsYAML(addon.getId(), getWidgetFromURL(yamlDownloadUrl));
} else if (yamlContent != null) {
addWidgetAsYAML(addon.getId(), yamlContent);
} else {
throw new IllegalArgumentException("Couldn't find the block library in the add-on entry");
}
} catch (IOException e) {
logger.error("Block library from marketplace cannot be downloaded: {}", e.getMessage());
throw new MarketplaceHandlerException("Widget cannot be downloaded.", e);
} catch (Exception e) {
logger.error("Block library from marketplace is invalid: {}", e.getMessage());
throw new MarketplaceHandlerException("Widget is not valid.", e);
}
}
@Override
public void uninstall(Addon addon) throws MarketplaceHandlerException {
blocksRegistry.getAll().stream().filter(w -> w.hasTag(addon.getId())).forEach(w -> {
blocksRegistry.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 block library in the registry
widget.addTag(id);
blocksRegistry.add(widget);
} catch (IOException e) {
logger.error("Unable to parse YAML: {}", e.getMessage());
throw new IllegalArgumentException("Unable to parse YAML");
}
}
}
@@ -75,6 +75,7 @@ public class CommunityMarketplaceAddonService implements AddonService {
public static final String KAR_CONTENT_TYPE = "application/vnd.openhab.feature;type=karfile";
public static final String RULETEMPLATES_CONTENT_TYPE = "application/vnd.openhab.ruletemplate";
public static final String UIWIDGETS_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=widget";
public static final String BLOCKLIBRARIES_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=blocks";
// constants for the configuration properties
static final String CONFIG_URI = "system:marketplace";
@@ -94,6 +95,7 @@ public class CommunityMarketplaceAddonService implements AddonService {
private static final Integer BUNDLES_CATEGORY = 73;
private static final Integer RULETEMPLATES_CATEGORY = 74;
private static final Integer UIWIDGETS_CATEGORY = 75;
private static final Integer BLOCKLIBRARIES_CATEGORY = 76;
private static final String PUBLISHED_TAG = "published";
@@ -281,6 +283,8 @@ public class CommunityMarketplaceAddonService implements AddonService {
return TAG_ADDON_TYPE_MAP.get("automation");
} else if (UIWIDGETS_CATEGORY.equals(category)) {
return TAG_ADDON_TYPE_MAP.get("ui");
} else if (BLOCKLIBRARIES_CATEGORY.equals(category)) {
return TAG_ADDON_TYPE_MAP.get("automation");
} else if (BUNDLES_CATEGORY.equals(category)) {
// try to get it from tags if we have tags
return tags.stream().map(TAG_ADDON_TYPE_MAP::get).filter(Objects::nonNull).findFirst().orElse(null);
@@ -296,6 +300,8 @@ public class CommunityMarketplaceAddonService implements AddonService {
return RULETEMPLATES_CONTENT_TYPE;
} else if (UIWIDGETS_CATEGORY.equals(category)) {
return UIWIDGETS_CONTENT_TYPE;
} else if (BLOCKLIBRARIES_CATEGORY.equals(category)) {
return BLOCKLIBRARIES_CONTENT_TYPE;
} else if (BUNDLES_CATEGORY.equals(category)) {
if (tags.contains("kar")) {
return KAR_CONTENT_TYPE;