RestDocsServlet.java

/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community 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://opensource.org/licenses/ecl2.txt
 *
 * 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.
 *
 */

package org.opencastproject.runtimeinfo;

import static org.opencastproject.rest.RestConstants.SERVICES_FILTER;
import static org.opencastproject.rest.RestConstants.SERVICE_PATH_PROPERTY;

import org.opencastproject.runtimeinfo.rest.RestDocData;
import org.opencastproject.systems.OpencastConstants;
import org.opencastproject.util.doc.DocUtil;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestService;

import org.apache.commons.lang3.StringUtils;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardContextSelect;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletName;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletPattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

/** A bundle activator that registers the REST documentation servlet. */
@Component(service = Servlet.class)
@HttpWhiteboardServletName(RestDocsServlet.SERVLET_PATH)
@HttpWhiteboardServletPattern(RestDocsServlet.SERVLET_PATH)
@HttpWhiteboardContextSelect("(osgi.http.whiteboard.context.name=opencast)")
public class RestDocsServlet extends HttpServlet {

  /** The logger */
  private static final Logger logger = LoggerFactory.getLogger(RestDocsServlet.class);

  /** The servlet path */
  public static final String SERVLET_PATH = "/docs.html";

  /** The query string parameter used to specify a specific service */
  private static final String PATH_PARAM = "path";

  /** java.io serialization UID */
  private static final long serialVersionUID = 6930336096831297329L;

  /** The OSGI bundle context */
  protected BundleContext bundleContext;

  /** A map of global macro values for REST documentation. */
  private Map<String, String> globalMacro;

  @Activate
  public void start(BundleContext bundleContext) throws Exception {
    this.bundleContext = bundleContext;
    prepareMacros();
  }

  /** Add a list of global information, such as the server URL, to the globalMacro map. */
  private void prepareMacros() {
    globalMacro = new HashMap<String, String>();
    globalMacro.put("PING_BACK_URL", bundleContext.getProperty("org.opencastproject.anonymous.feedback.url"));
    globalMacro.put("HOST_URL", bundleContext.getProperty(OpencastConstants.SERVER_URL_PROPERTY));
    globalMacro.put("LOCAL_STORAGE_DIRECTORY", bundleContext.getProperty("org.opencastproject.storage.dir"));
  }

  public void stop(BundleContext bundleContext) throws Exception {
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String docPath = req.getParameter(PATH_PARAM);
    if (StringUtils.isBlank(docPath)) {
      resp.sendRedirect("rest_docs.html");
    } else {
      // write the details for this service
      writeServiceDocumentation(docPath, resp);
    }
  }

  private void writeServiceDocumentation(final String docPath, HttpServletResponse resp)
          throws IOException {
    ServiceReference reference = null;
    for (ServiceReference ref : getRestEndpointServices()) {
      String alias = (String) ref.getProperty(SERVICE_PATH_PROPERTY);
      if (docPath.equalsIgnoreCase(alias)) {
        reference = ref;
        break;
      }
    }

    final StringBuilder docs = new StringBuilder();

    if (reference == null) {
      docs.append("REST docs unavailable for ");
      docs.append(docPath);
    } else {
      final Object restService = bundleContext.getService(reference);
      findRestAnnotation(restService.getClass()).ifPresentOrElse(
          annotation -> {
            globalMacro.put("SERVICE_CLASS_SIMPLE_NAME", restService.getClass().getSimpleName());
            RestDocData data = new RestDocData(annotation.name(), annotation.title(), docPath, annotation.notes());
            data.setAbstract(annotation.abstractText());

            Produces producesClass = (Produces) restService.getClass().getAnnotation(Produces.class);

            for (Method m : restService.getClass().getMethods()) {
              RestQuery rq = (RestQuery) m.getAnnotation(RestQuery.class);
              String httpMethodString = null;
              for (Annotation a : m.getAnnotations()) {
                HttpMethod httpMethod = (HttpMethod) a.annotationType().getAnnotation(HttpMethod.class);
                if (httpMethod != null) {
                  httpMethodString = httpMethod.value();
                }
              }
              Produces produces = (Produces) m.getAnnotation(Produces.class);
              if (produces == null) {
                produces = producesClass;
              }
              Path path = (Path) m.getAnnotation(Path.class);
              Class<?> returnType = m.getReturnType();
              if ((rq != null) && (httpMethodString != null) && (path != null)) {
                data.addEndpoint(rq, returnType, produces, httpMethodString, path);
              }
            }
            String template = DocUtil.loadTemplate("/ui/restdocs/template.xhtml");
            docs.append(DocUtil.generate(data, template));
          },
          () -> docs.append("No documentation has been found for ").append(restService.getClass().getSimpleName())
      );
    }

    resp.setContentType("text/html");
    resp.getWriter().write(docs.toString());
  }

  private ServiceReference[] getRestEndpointServices() {
    try {
      return bundleContext.getAllServiceReferences(null, SERVICES_FILTER);
    } catch (InvalidSyntaxException e) {
      logger.warn("Unable to query the OSGI service registry for all registered rest endpoints");
      return new ServiceReference[0];
    }
  }

  /** Try to find the RestService annotation starting at <code>endpointClass</code>. */
  public static Optional<RestService> findRestAnnotation(Class<?> endpointClass) {
    if (endpointClass == null) {
      return Optional.empty();
    }
    final RestService rs = endpointClass.getAnnotation(RestService.class);
    if (rs == null) {
      return findRestAnnotation(endpointClass.getSuperclass());
    } else {
      return Optional.of(rs);
    }
  }
}