/** * Copyright (c) Codice Foundation * * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. * **/ package org.codice.ddf.spatial.kml.endpoint; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; import java.util.Map; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilderException; import javax.ws.rs.core.UriInfo; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.webconsole.BrandingPlugin; import org.codice.ddf.configuration.ConfigurationManager; import org.codice.ddf.configuration.ConfigurationWatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.Template; import com.github.jknack.handlebars.io.ClassPathTemplateLoader; import ddf.catalog.CatalogFramework; import ddf.catalog.operation.SourceInfoResponse; import ddf.catalog.operation.impl.SourceInfoRequestEnterprise; import ddf.catalog.source.SourceDescriptor; import ddf.catalog.source.SourceUnavailableException; import de.micromata.opengis.kml.v_2_2_0.Folder; import de.micromata.opengis.kml.v_2_2_0.Kml; import de.micromata.opengis.kml.v_2_2_0.KmlFactory; import de.micromata.opengis.kml.v_2_2_0.Link; import de.micromata.opengis.kml.v_2_2_0.NetworkLink; import de.micromata.opengis.kml.v_2_2_0.RefreshMode; import de.micromata.opengis.kml.v_2_2_0.ViewRefreshMode; /** * Endpoint used to create KML {@link NetworkLink}s. The KML Network Link will link Google Earth to * the Catalog through the OpenSearch Endpoint. * * @author Keith C Wire * */ @Path("/") public class KmlEndpoint implements ConfigurationWatcher { private static final String FORWARD_SLASH = "/"; private static final String CATALOG_URL_PATH = "catalog"; private static final String KML_MIME_TYPE = "application/vnd.google-earth.kml+xml"; private static final String KML_TRANSFORM_PARAM = "kml"; private static final String OPENSEARCH_URL_PATH = "query"; private static final String OPENSEARCH_SORT_KEY = "sort"; private static final String OPENSEARCH_DEFAULT_SORT = "date:desc"; private static final String OPENSEARCH_FORMAT_KEY = "format"; private static final String ICONS_RESOURCE_LOC = "icons/"; private static final long REFRESH_INTERVAL = 12 * 60 * 60; // 12 Hours in Seconds /** Default refresh time after the View stops moving */ private static final double DEFAULT_VIEW_REFRESH_TIME = 2.0; /** * The format of the bounding box query parameters Google Earth attaches to the end of the query * URL. */ private static final String VIEW_FORMAT_STRING = "bbox=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]"; private static final String SOURCE_PARAM = "src"; private static final String COUNT_PARAM = "count="; private static final Logger LOGGER = LoggerFactory.getLogger(KmlEndpoint.class); private String host; private String port; private BrandingPlugin branding; private CatalogFramework framework; private String servicesContextRoot; private Kml styleDoc; private String styleUrl; private String iconLoc; private String description; private Boolean visibleByDefault = false; private Integer maxResults = 100; private String webSite; private String logo; private String productName; private String contact; private String baseUrl; private ClassPathTemplateLoader templateLoader; public KmlEndpoint(BrandingPlugin brandingPlugin, CatalogFramework catalogFramework) { LOGGER.trace("ENTERING: KML Endpoint Constructor"); this.branding = brandingPlugin; this.framework = catalogFramework; templateLoader = new ClassPathTemplateLoader(); templateLoader.setPrefix("/templates"); templateLoader.setSuffix(".hbt"); this.productName = branding.getProductName().split(" ")[0]; LOGGER.trace("EXITING: KML Endpoint Constructor"); } /** * Attempts to load a KML {@link de.micromata.opengis.kml.v_2_2_0.Style} from a file provided via a file system path. * * @param url * - the path to the file. */ public void setStyleUrl(String url) { if (StringUtils.isNotBlank(url)) { try { styleDoc = null; styleUrl = url; styleDoc = Kml.unmarshal(new URL(styleUrl).openStream()); } catch (MalformedURLException e) { LOGGER.warn("StyleUrl is not a valid URL. Unable to serve up custom KML de.micromata.opengis.kml.v_2_2_0.Style.", e); } catch (IOException e) { LOGGER.warn("Unable to open de.micromata.opengis.kml.v_2_2_0.Style Document from StyleUrl.", e); } } } /** * Sets the root directory of icons to be provided via this endpoint. * * @param iconLoc * - the path to the directory of icons */ public void setIconLoc(String iconLoc) { this.iconLoc = iconLoc; } public String getDescription() { return this.description; } /** * Sets the Description that will be used as the description of the Root {@link NetworkLink}. * * @param description * - the Description of the Root {@link NetworkLink} */ public void setDescription(String description) { this.description = description; } /** * Sets if the ddf.catalog.source.Source {@link NetworkLink}s should be Visible by Default. * * @param visibleByDefault * - true to enable */ public void setVisibleByDefault(Boolean visibleByDefault) { this.visibleByDefault = visibleByDefault; } /** * Sets the Maximum Number of results each {@link NetworkLink} will return. * * @param maxResults * - maximum number of results to return */ public void setMaxResults(Integer maxResults) { this.maxResults = maxResults; } public String getWebSite() { return this.webSite; } /** * Sets the Web Site URL that will be used in the description of the Root {@link NetworkLink}. * * @param webSite * - the URL of the web site */ public void setWebSite(String webSite) { this.webSite = webSite; } public String getLogo() { return this.logo; } /** * Sets the URL of the Logo that will be used in the description of the Root {@link NetworkLink} * . * * @param logo * - the URL to the logo */ public void setLogo(String logo) { this.logo = logo; } public String getProductName() { return this.productName; } public String getContact() { return this.contact; } public String getBaseUrl() { return this.baseUrl; } /** * Creates a {@link NetworkLink} to provide a layer to KML Clients. * * @param uriInfo * - injected resource providing the URI. * @return - KML NetworkLink */ @GET @Path(FORWARD_SLASH) @Produces(KML_MIME_TYPE) public Kml getKmlNetworkLink(@Context UriInfo uriInfo) { LOGGER.debug("ENTERING: getKmlNetworkLink"); try { return createRootNetworkLink(uriInfo); } catch (UnknownHostException e) { throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } } private Kml createRootNetworkLink(UriInfo uriInfo) throws UnknownHostException { Kml kml = KmlFactory.createKml(); NetworkLink rootNetworkLink = kml.createAndSetNetworkLink(); rootNetworkLink.setName(this.productName); rootNetworkLink.setSnippet(KmlFactory.createSnippet().withMaxLines(0)); UriBuilder baseUrlBuidler = UriBuilder.fromUri(uriInfo.getBaseUri()); baseUrlBuidler.replacePath(""); this.baseUrl = baseUrlBuidler.build().toString(); String descriptionHtml = description; Handlebars handlebars = new Handlebars(templateLoader); try { Template template = handlebars.compile("description"); descriptionHtml = template.apply(this); LOGGER.debug(descriptionHtml); } catch (IOException e) { LOGGER.error("Failed to apply description Template", e); } rootNetworkLink.setDescription(descriptionHtml); rootNetworkLink.setOpen(true); rootNetworkLink.setVisibility(false); Link link = rootNetworkLink.createAndSetLink(); UriBuilder builder = UriBuilder.fromUri(uriInfo.getBaseUri()); builder = generateEndpointUrl( servicesContextRoot + FORWARD_SLASH + CATALOG_URL_PATH + FORWARD_SLASH + KML_TRANSFORM_PARAM + FORWARD_SLASH + "sources", builder); link.setHref(builder.build().toString()); link.setViewRefreshMode(ViewRefreshMode.NEVER); link.setRefreshMode(RefreshMode.ON_INTERVAL); link.setRefreshInterval(REFRESH_INTERVAL); return kml; } /** * Creates a list of {@link NetworkLink}s, one for each {@link ddf.catalog.source.Source} including the local * catalog. * * @param uriInfo * - injected resource provding the URI. * @return - {@link Kml} containing a folder of {@link NetworkLink}s. */ @GET @Path(FORWARD_SLASH + "sources") @Produces(KML_MIME_TYPE) public Kml getAvailableSources(@Context UriInfo uriInfo) { try { SourceInfoResponse response = framework .getSourceInfo(new SourceInfoRequestEnterprise(false)); Kml kml = KmlFactory.createKml(); Folder folder = kml.createAndSetFolder(); folder.setOpen(true); for (SourceDescriptor descriptor : response.getSourceInfo()) { UriBuilder builder = UriBuilder.fromUri(uriInfo.getBaseUri()); builder = generateEndpointUrl( servicesContextRoot + FORWARD_SLASH + CATALOG_URL_PATH + FORWARD_SLASH + OPENSEARCH_URL_PATH, builder); builder = builder.queryParam(SOURCE_PARAM, descriptor.getSourceId()); builder = builder.queryParam(OPENSEARCH_SORT_KEY, OPENSEARCH_DEFAULT_SORT); builder = builder.queryParam(OPENSEARCH_FORMAT_KEY, KML_TRANSFORM_PARAM); NetworkLink networkLink = generateViewBasedNetworkLink(builder.build().toURL(), descriptor.getSourceId()); folder.getFeature().add(networkLink); } return kml; } catch (SourceUnavailableException e) { throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } catch (UnknownHostException e) { throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } catch (MalformedURLException e) { throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } catch (IllegalArgumentException e) { throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } catch (UriBuilderException e) { throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } } /* * Generates xml for View-based Network Link * * @param networkLinkUrl - url to set as the Link href. * * @return Networklink */ private NetworkLink generateViewBasedNetworkLink(URL networkLinkUrl, String sourceId) { // create network link and give it a name NetworkLink networkLink = KmlFactory.createNetworkLink(); networkLink.setName(sourceId); networkLink.setOpen(true); networkLink.setVisibility(this.visibleByDefault); // create link and add it to networkLinkElements Link link = networkLink.createAndSetLink(); LOGGER.debug("View Based Network Link href: {}", networkLinkUrl.toString()); link.setHref(networkLinkUrl.toString()); link.setViewRefreshMode(ViewRefreshMode.ON_STOP); link.setViewRefreshTime(DEFAULT_VIEW_REFRESH_TIME); link.setViewFormat(VIEW_FORMAT_STRING); link.setViewBoundScale(1); link.setHttpQuery(COUNT_PARAM + maxResults); return networkLink; } /* * Creates the URL based on the configured host, port, and services context root path. */ private UriBuilder generateEndpointUrl(String path, UriBuilder uriBuilder) throws UnknownHostException { UriBuilder builder = uriBuilder; if (host != null && port != null && servicesContextRoot != null) { builder = builder.host(host); try { int portInt = Integer.parseInt(port); builder = builder.port(portInt); } catch (NumberFormatException nfe) { LOGGER.debug("Cannot convert the current DDF port: {} to an integer." + " Defaulting to port in invocation.", port); throw new UnknownHostException("Unable to determine port DDF is using."); } builder = builder.replacePath(path); } else { LOGGER.debug("DDF Port is null, unable to determine host DDF is running on."); throw new UnknownHostException("Unable to determine port DDF is using."); } return builder; } /** * Kml REST Get. Returns the style Document. * * @param uriInfo * @return stylesDoc * @throws WebApplicationException */ @GET @Path(FORWARD_SLASH + "styles") @Produces(KML_MIME_TYPE) public Kml getKmlStyles(@Context UriInfo uriInfo) { if (styleDoc != null) { return styleDoc; } throw new WebApplicationException(new FileNotFoundException( "No KML de.micromata.opengis.kml.v_2_2_0.Style has been configured or unable to load document."), Status.NOT_FOUND); } /** * Retrieves an icon from the hosted directory based on the id provided. * * @param uriInfo * - injected resource providing the URI * @param id * - the id (filename) of the icon * @return iconBytes - the icon as a byte[] */ @GET @Path("/icons/{id:.+}") @Produces({"image/png", "image/jpeg", "image/tiff", "image/gif"}) public byte[] getIcon(@Context UriInfo uriInfo, @PathParam("id") String id) { byte[] iconBytes = null; if (StringUtils.isBlank(iconLoc)) { String icon = ICONS_RESOURCE_LOC + id; try (InputStream iconStream = this.getClass().getClassLoader() .getResourceAsStream(icon)) { if (iconStream == null) { LOGGER.warn("Resource not found for icon {}", icon); throw new WebApplicationException( new FileNotFoundException("Resource not found for icon " + icon), Status.NOT_FOUND); } iconBytes = IOUtils.toByteArray(iconStream); } catch (IOException e) { LOGGER.warn("Failed to read resource for icon " + icon, e); throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } } else { String icon = iconLoc + FORWARD_SLASH + id; try (InputStream message = new FileInputStream(icon)) { iconBytes = IOUtils.toByteArray(message); } catch (FileNotFoundException e) { LOGGER.warn("File not found for icon " + icon, e); throw new WebApplicationException(e, Status.NOT_FOUND); } catch (IOException e) { LOGGER.warn("Failed to read bytes for icon " + icon, e); throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } } return iconBytes; } @Override public void configurationUpdateCallback(Map<String, String> configuration) { String methodName = "configurationUpdateCallback"; LOGGER.debug("ENTERING: {}", methodName); if (configuration != null && !configuration.isEmpty()) { Object value = configuration.get(ConfigurationManager.HOST); if (value != null) { this.host = value.toString(); LOGGER.debug("ddfHost = {}", this.host); } else { LOGGER.debug("ddfHost = NULL"); } value = configuration.get(ConfigurationManager.PORT); if (value != null) { this.port = value.toString(); LOGGER.debug("ddfPort = {}", this.port); } else { LOGGER.debug("ddfPort = NULL"); } value = configuration.get(ConfigurationManager.SERVICES_CONTEXT_ROOT); if (value != null) { this.servicesContextRoot = value.toString(); LOGGER.debug("servicesContextRoot = {}", this.servicesContextRoot); } else { LOGGER.debug("servicesContextRoot = NULL"); } value = configuration.get(ConfigurationManager.CONTACT); if (value != null) { this.contact = value.toString(); LOGGER.debug("contact = {}", this.contact); } else { LOGGER.debug("contact = NULL"); } } else { LOGGER.debug("properties are NULL or empty"); } LOGGER.debug("EXITING: {}", methodName); } }