ServletContainerBridge.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.caravan.jaxrs.publisher.impl;

import java.io.IOException;
import java.util.Set;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Sets;

import io.wcm.caravan.jaxrs.publisher.JaxRsClassesProvider;
import io.wcm.caravan.jaxrs.publisher.JaxRsComponent;

/**
 * Servlet bridge that wraps a Jersy JAX-RS ServletContainer and ensures the JAX-RS context is reloaded
 * when service registrations (local or global) change.
 * It automatically registers JAX-RS components from the current bundle and instances
 * from global JAX-RS components factories.
 */
@Component(service = Servlet.class, factory = ServletContainerBridge.SERVLETCONTAINER_BRIDGE_FACTORY)
public class ServletContainerBridge extends HttpServlet {
  private static final long serialVersionUID = 1L;

  static final String SERVLETCONTAINER_BRIDGE_FACTORY = "caravan.jaxrs.servletcontainer.bridge.factory";
  static final String PROPERTY_BUNDLE_ID = "caravan.jaxrs.relatedBundleId";

  private BundleContext bundleContext;
  private Bundle bundle;
  private JaxRsApplication application;
  private ServletContainer servletContainer;
  private volatile boolean isDirty;
  private Set<JaxRsComponent> localComponents;
  private Set<JaxRsComponent> globalComponents;
  private ServiceTracker<JaxRsComponent, Object> jaxRsComponentTracker;
  private Set<JaxRsClassesProvider> localClassesProviders;
  private Set<JaxRsClassesProvider> globalClassesProviders;
  private ServiceTracker<JaxRsClassesProvider, Object> jaxClassesProviderTracker;

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

  @Activate
  void activate(ComponentContext componentContext) {
    // bundle which contains the JAX-RS services
    bundle = componentContext.getBundleContext().getBundle(
        (Long)componentContext.getProperties().get(PROPERTY_BUNDLE_ID));
    bundleContext = bundle.getBundleContext();

    // initialize component tracker to detect local and global JAX-RS components for current bundle
    localComponents = Sets.newConcurrentHashSet();
    globalComponents = Sets.newConcurrentHashSet();
    jaxRsComponentTracker = new JaxRsComponentTracker(this);
    jaxRsComponentTracker.open();

    // initialize component tracker to detect local and global additionall JAX-RS classes
    localClassesProviders = Sets.newConcurrentHashSet();
    globalClassesProviders = Sets.newConcurrentHashSet();
    jaxClassesProviderTracker = new JaxRsClassesProviderTracker(this);
    jaxClassesProviderTracker.open();
  }

  @Deactivate
  void deactivate() {
    if (jaxRsComponentTracker != null) {
      jaxRsComponentTracker.close();
    }
    if (jaxClassesProviderTracker != null) {
      jaxClassesProviderTracker.close();
    }
  }

  @Override
  public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
    reloadIfDirty();
    // delegate all calls to jersey servlet container
    servletContainer.service(request, response);
  }

  @Override
  public void init() throws ServletException {
    // initialize JAX-RS application and Jersey Servlet container
    application = new JaxRsApplication(localComponents, globalComponents,
        localClassesProviders, globalClassesProviders);
    servletContainer = new ServletContainer(ResourceConfig.forApplication(application));
    servletContainer.init(getServletConfig());
  }

  @Override
  public void destroy() {
    servletContainer.destroy();
  }

  /**
   * Checks if components where added or removed.
   * If yes the jersey servlet container is reloaded with the new configuration, blocking all other calls in the
   * meantime.
   */
  private void reloadIfDirty() {
    if (isDirty) {
      synchronized (this) {
        if (isDirty) {
          // reload configuration if any service requests have changed
          log.debug("Reload JAX-RS servlet container of {}", bundle.getSymbolicName());
          servletContainer.reload(ResourceConfig.forApplication(application));
          isDirty = false;
        }
      }
    }
  }

  @Override
  public String toString() {
    return "jaxrs-servlet:" + bundle.getSymbolicName();
  }

  BundleContext getBundleContext() {
    return this.bundleContext;
  }

  Bundle getBundle() {
    return this.bundle;
  }

  Set<JaxRsComponent> getLocalComponents() {
    return this.localComponents;
  }

  Set<JaxRsComponent> getGlobalComponents() {
    return this.globalComponents;
  }

  Set<JaxRsClassesProvider> getLocalClassesProviders() {
    return this.localClassesProviders;
  }

  Set<JaxRsClassesProvider> getGlobalClassesProviders() {
    return this.globalClassesProviders;
  }

  void markAsDirty() {
    this.isDirty = true;
  }

  static boolean isJaxRsGlobal(ServiceReference<?> serviceReference) {
    return "true".equals(serviceReference.getProperty(JaxRsComponent.PROPERTY_GLOBAL_COMPONENT))
        && Constants.SCOPE_BUNDLE.equals(serviceReference.getProperty(Constants.SERVICE_SCOPE));
  }

}