AssetService.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2015 wcm.io
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package io.wcm.dam.assetservice.impl;

import java.util.Dictionary;
import java.util.Hashtable;

import javax.servlet.Servlet;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.osgi.service.metatype.annotations.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.dam.api.DamConstants;
import com.day.cq.dam.api.DamEvent;

import io.wcm.dam.assetservice.impl.dataversion.ChecksumDataVersionStrategy;
import io.wcm.dam.assetservice.impl.dataversion.TimestampDataVersionStrategy;
import io.wcm.wcm.commons.contenttype.FileExtension;

/**
 * Implements a simple REST interface that allows resolving DAM asset paths to URLs.
 * For image assets resolving to specific dimensions is supported.
 */
@Component(immediate = true, service = { AssetService.class, EventHandler.class },
    property = EventConstants.EVENT_TOPIC + "=" + DamEvent.EVENT_TOPIC)
@Designate(ocd = AssetService.Config.class)
public class AssetService implements EventHandler {

  @ObjectClassDefinition(name = "wcm.io DAM Asset Service",
      description = "A RESTful service for resolving URLs to DAM assets and renditions.")
  @interface Config {

    @AttributeDefinition(name = "Asset Selector", description = "Selector for attaching REST service to DAM asset paths.")
    String assetServletSelector() default "wcm-io-asset-service";

    @AttributeDefinition(name = "Data Version Selector", description = "Selector for attaching REST service to DAM folder for getting data version.")
    String dataVersionServletSelector() default "wcm-io-asset-service-dataversion";

    @AttributeDefinition(name = "Data Version Strategy", description = "Strategy for building the data versions. See documentation for details.",
        options = {
            @Option(label = TimestampDataVersionStrategy.STRATEGY + ": Timestamp of last DAM event", value = TimestampDataVersionStrategy.STRATEGY),
            @Option(label = ChecksumDataVersionStrategy.STRATEGY + ": Aggregated checksum of DAM assets", value = ChecksumDataVersionStrategy.STRATEGY)
        })
    String dataVersionStrategy() default TimestampDataVersionStrategy.STRATEGY;

    @AttributeDefinition(name = "Update Interval (sec)", description = "Updating interval for calculating data versions in seconds. "
        + "If multiple changes to the DAM folders contents are detected within this interval they are collected. "
        + "This is only used by the 'aggregated checksum' strategy.")
    int dataVersionUpdateIntervalSec() default 60;

    @AttributeDefinition(name = "DAM paths", description = "List of DAM paths for which the asset service should be active. "
        + "If not set, the service is active for all paths.")
    String[] damPaths();

  }

  @Reference
  private ResourceResolverFactory resourceResolverFactory;

  private DamPathHandler damPathHandler;
  private BundleContext bundleContext;
  private ServiceRegistration<Servlet> assetRequestServletReg;
  private ServiceRegistration<Servlet> dataVersionServletReg;

  private static final Logger log = LoggerFactory.getLogger(AssetService.class);

  @Activate
  protected void activate(BundleContext context, Config config) {
    log.info("Start wcm.io DAM Asset Service.");

    this.bundleContext = context;

    String assetServletSelector = config.assetServletSelector();
    String dataVersionServletSelector = config.dataVersionServletSelector();
    int dataVersionUpdateIntervalSec = config.dataVersionUpdateIntervalSec();

    String[] damPaths = config.damPaths();
    String dataVersionStrategyId = config.dataVersionStrategy();
    damPathHandler = new DamPathHandler(damPaths, dataVersionStrategyId, dataVersionUpdateIntervalSec, resourceResolverFactory);

    // register servlets to resource types to handle the JSON requests
    // they are registered dynamically because the selectors are configurable
    assetRequestServletReg = registerServlet(context, new AssetRequestServlet(damPathHandler),
        DamConstants.NT_DAM_ASSET, assetServletSelector);
    dataVersionServletReg = registerServlet(context, new DataVersionServlet(damPathHandler),
        "sling:OrderedFolder", dataVersionServletSelector);
  }

  @Deactivate
  protected void deactivate() {
    log.info("Shutdown wcm.io DAM Asset Service.");

    assetRequestServletReg.unregister();
    dataVersionServletReg.unregister();
    damPathHandler.shutdown();
  }

  @Override
  public void handleEvent(Event event) {
    if (!StringUtils.equals(event.getTopic(), DamEvent.EVENT_TOPIC)) {
      return;
    }
    DamEvent damEvent = DamEvent.fromEvent(event);
    damPathHandler.handleDamEvent(damEvent);
  }

  AssetRequestServlet getAssetRequestServlet() {
    return (AssetRequestServlet)bundleContext.getService(assetRequestServletReg.getReference());
  }

  DataVersionServlet getDataVersionServlet() {
    return (DataVersionServlet)bundleContext.getService(dataVersionServletReg.getReference());
  }

  @SuppressWarnings("null")
  private static <T extends Servlet> ServiceRegistration<Servlet> registerServlet(BundleContext bundleContext, T servletInstance,
      String resourceType, String selector) {
    if (StringUtils.isEmpty(selector)) {
      throw new IllegalArgumentException("No selector defined for " + servletInstance.getClass().getName() + " - skipping servlet registration.");
    }
    Dictionary<String, Object> config = new Hashtable<>();
    config.put("sling.servlet.resourceTypes", resourceType);
    config.put("sling.servlet.selectors", selector);
    config.put("sling.servlet.extensions", FileExtension.JSON);
    return bundleContext.registerService(Servlet.class, servletInstance, config);
  }

}