BundleInfoRestEndpoint.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.kernel.bundleinfo;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
import static org.opencastproject.util.EqualsUtil.ne;
import static org.opencastproject.util.Jsons.arr;
import static org.opencastproject.util.Jsons.obj;
import static org.opencastproject.util.Jsons.p;
import static org.opencastproject.util.RestUtil.R.notFound;
import static org.opencastproject.util.RestUtil.R.ok;
import static org.opencastproject.util.data.Collections.set;
import static org.opencastproject.util.data.Collections.toArray;

import org.opencastproject.util.Jsons;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;

import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

/** Bundle information via REST. */
@RestService(
  name = "systemInfo",
  title = "System Bundle Info",
  notes = { "This is used to display the version information on the login page." },
  abstractText = "The system bundle info endpoint yields information about the running OSGi bundles of Opencast.")
public abstract class BundleInfoRestEndpoint {

  private static final Logger logger = LoggerFactory.getLogger(BundleInfoRestEndpoint.class);

  private static final String DEFAULT_BUNDLE_PREFIX = "opencast";

  protected abstract BundleInfoDb getDb();

  private long lastModified = 0;

  @Activate
  public void activate(ComponentContext cc) {
    lastModified = cc.getBundleContext().getBundle().getLastModified();
  }

  @GET
  // path prefix "bundles" is contained here and not in the path annotation of the class
  // See https://opencast.jira.com/browse/MH-9768
  @Path("bundles/list")
  @Produces(APPLICATION_JSON)
  @RestQuery(
    name = "list",
    description = "Return a list of all running bundles on the whole cluster.",
    responses = {
      @RestResponse(description = "A list of bundles.", responseCode = HttpServletResponse.SC_OK) },
    returnDescription = "The search results, expressed as xml or json.")
  public Response getVersions() {
    List<Jsons.Val> bundleInfos = getDb().getBundles().stream()
        .map(b -> (Jsons.Val) bundleInfo(b))
        .toList();

    return ok(obj(p("bundleInfos", arr(bundleInfos)), p("count", bundleInfos.size())));
  }

  /** Return true if all bundles have the same bundle version and build number. */
  @GET
  @Path("bundles/check")
  @RestQuery(
    name = "check",
    description = "Check if all bundles throughout the cluster have the same OSGi bundle version and build number.",
    restParameters = {
      @RestParameter(
        name = "prefix",
        description = "The bundle name prefixes to check. Defaults to '" + DEFAULT_BUNDLE_PREFIX + "'.",
        isRequired = false,
        defaultValue = DEFAULT_BUNDLE_PREFIX,
        type = RestParameter.Type.STRING) },
    responses = {
      @RestResponse(description = "true/false", responseCode = HttpServletResponse.SC_OK),
      @RestResponse(description = "cannot find any bundles with the given prefix", responseCode = HttpServletResponse.SC_NOT_FOUND) },
    returnDescription = "The search results, expressed as xml or json.")
  public Response checkBundles(@DefaultValue(DEFAULT_BUNDLE_PREFIX) @QueryParam("prefix") List<String> prefixes) {
    return withBundles(prefixes, infos -> {
      final String bundleVersion = infos.get(0).getBundleVersion();
      final Optional<String> buildNumber = infos.get(0).getBuildNumber();
      for (BundleInfo a : infos) {
        if (ne(a.getBundleVersion(), bundleVersion) || ne(a.getBuildNumber(), buildNumber))
          return ok(TEXT_PLAIN_TYPE, "false");
      }
      return ok(TEXT_PLAIN_TYPE, "true");
    });
  }

  /** Return the common version of all bundles matching the given prefix. */
  @GET
  @Path("bundles/version")
  @Produces(APPLICATION_JSON)
  @RestQuery(
    name = "bundleVersion",
    description = "Return the common OSGi build version and build number of all bundles matching the given prefix.",
    restParameters = {
      @RestParameter(
        name = "prefix",
        description = "The bundle name prefixes to check. Defaults to '" + DEFAULT_BUNDLE_PREFIX + "'.",
        isRequired = false,
        defaultValue = DEFAULT_BUNDLE_PREFIX,
        type = RestParameter.Type.STRING) },
    responses = {
      @RestResponse(description = "Version structure", responseCode = HttpServletResponse.SC_OK),
      @RestResponse(description = "No bundles with the given prefix", responseCode = HttpServletResponse.SC_NOT_FOUND) },
    returnDescription = "The search results as json.")
  public Response getBundleVersion(@DefaultValue(DEFAULT_BUNDLE_PREFIX) @QueryParam("prefix") List<String> prefixes) {
    return withBundles(prefixes, infos -> {
      final Set<BundleVersion> versions = set();
      for (BundleInfo bundle : infos) {
        versions.add(bundle.getVersion());
      }
      final BundleInfo example = infos.get(0);
      switch (versions.size()) {
        case 0:
          // no versions...
          throw new Error("bug");
        case 1:
          // all versions align
          return ok(obj(p("consistent", true))
              .append(fullVersionJson(example.getVersion()))
              .append(obj(p("last-modified", lastModified))));
        default:
          // multiple versions found
          return ok(obj(
              p("consistent", false),
              p("versions",
                  arr(StreamSupport.stream(versions.spliterator(), false)
                      .map(v -> (Jsons.Val) fullVersionJson(v))
                      .collect(Collectors.toList())))
          ));
      }
    });
  }

  @DELETE
  @Path("bundles/host")
  @RestQuery(
          name = "clearHost",
          description = "Removes the tracked bundles for a host. This is done automatically when you shut down "
          + "Opencast. But this endpoint can be used to force this in case e.g. a machine got dropped. Make sure the "
          + "host is actually gone! The database will be automatically rebuilt when Opencast on that host is "
          + "(re)started.",
          restParameters = {
                  @RestParameter(
                          name = "host",
                          description = "The name of the host to clear",
                          isRequired = true,
                          type = RestParameter.Type.STRING,
                          defaultValue = "") },
          responses = {
                  @RestResponse(description = "Version structure", responseCode = HttpServletResponse.SC_NO_CONTENT) },
          returnDescription = "No data is returned.")
  public Response clearHost(@QueryParam("host") String host) {
    logger.debug("Removing tracked bundles of host: {}", host);
    getDb().clear(host);
    return Response.noContent().build();
  }

  public static final Jsons.Obj fullVersionJson(BundleVersion version) {
    return obj(p("version", version.getBundleVersion()), p("buildNumber", version.getBuildNumber().map(Jsons::stringVal)));
  }

  public static Jsons.Obj bundleInfoJson(BundleInfo bundle) {
    return obj(p("host", bundle.getHost()), p("bundleSymbolicName", bundle.getBundleSymbolicName()),
            p("bundleId", bundle.getBundleId())).append(fullVersionJson(bundle.getVersion()));
  }

  public static final Jsons.Obj bundleInfo(BundleInfo bundle) {
    return bundleInfoJson(bundle);
  }

  /** Run <code>f</code> if there is at least one bundle matching the given prefixes. */
  private Response withBundles(List<String> prefixes, Function<List<BundleInfo>, Response> f) {
    final List<BundleInfo> info = getDb().getBundles(toArray(String.class, prefixes));
    if (info.size() > 0) {
      return f.apply(info);
    } else {
      return notFound("No bundles match one of the given prefixes");
    }
  }
}