View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.kernel.bundleinfo;
23  
24  import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
25  import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
26  import static org.opencastproject.util.EqualsUtil.ne;
27  import static org.opencastproject.util.Jsons.arr;
28  import static org.opencastproject.util.Jsons.obj;
29  import static org.opencastproject.util.Jsons.p;
30  import static org.opencastproject.util.Jsons.stringVal;
31  import static org.opencastproject.util.RestUtil.R.notFound;
32  import static org.opencastproject.util.RestUtil.R.ok;
33  import static org.opencastproject.util.data.Collections.set;
34  import static org.opencastproject.util.data.Collections.toArray;
35  import static org.opencastproject.util.data.Monadics.mlist;
36  
37  import org.opencastproject.util.Jsons;
38  import org.opencastproject.util.data.Function;
39  import org.opencastproject.util.data.Monadics;
40  import org.opencastproject.util.data.Option;
41  import org.opencastproject.util.data.functions.Functions;
42  import org.opencastproject.util.doc.rest.RestParameter;
43  import org.opencastproject.util.doc.rest.RestQuery;
44  import org.opencastproject.util.doc.rest.RestResponse;
45  import org.opencastproject.util.doc.rest.RestService;
46  
47  import org.osgi.service.component.ComponentContext;
48  import org.osgi.service.component.annotations.Activate;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import java.util.List;
53  import java.util.Set;
54  
55  import javax.servlet.http.HttpServletResponse;
56  import javax.ws.rs.DELETE;
57  import javax.ws.rs.DefaultValue;
58  import javax.ws.rs.GET;
59  import javax.ws.rs.Path;
60  import javax.ws.rs.Produces;
61  import javax.ws.rs.QueryParam;
62  import javax.ws.rs.core.Response;
63  
64  /** Bundle information via REST. */
65  @RestService(
66    name = "systemInfo",
67    title = "System Bundle Info",
68    notes = { "This is used to display the version information on the login page." },
69    abstractText = "The system bundle info endpoint yields information about the running OSGi bundles of Opencast.")
70  public abstract class BundleInfoRestEndpoint {
71  
72    private static final Logger logger = LoggerFactory.getLogger(BundleInfoRestEndpoint.class);
73  
74    private static final String DEFAULT_BUNDLE_PREFIX = "opencast";
75  
76    protected abstract BundleInfoDb getDb();
77  
78    private long lastModified = 0;
79  
80    @Activate
81    public void activate(ComponentContext cc) {
82      lastModified = cc.getBundleContext().getBundle().getLastModified();
83    }
84  
85    @GET
86    // path prefix "bundles" is contained here and not in the path annotation of the class
87    // See https://opencast.jira.com/browse/MH-9768
88    @Path("bundles/list")
89    @Produces(APPLICATION_JSON)
90    @RestQuery(
91      name = "list",
92      description = "Return a list of all running bundles on the whole cluster.",
93      responses = {
94        @RestResponse(description = "A list of bundles.", responseCode = HttpServletResponse.SC_OK) },
95      returnDescription = "The search results, expressed as xml or json.")
96    public Response getVersions() {
97      final Monadics.ListMonadic<Jsons.Val> bundleInfos = mlist(getDb().getBundles()).map(
98              Functions.<BundleInfo, Jsons.Val> co(bundleInfo));
99      return ok(obj(p("bundleInfos", arr(bundleInfos)), p("count", bundleInfos.value().size())));
100   }
101 
102   /** Return true if all bundles have the same bundle version and build number. */
103   @GET
104   @Path("bundles/check")
105   @RestQuery(
106     name = "check",
107     description = "Check if all bundles throughout the cluster have the same OSGi bundle version and build number.",
108     restParameters = {
109       @RestParameter(
110         name = "prefix",
111         description = "The bundle name prefixes to check. Defaults to '" + DEFAULT_BUNDLE_PREFIX + "'.",
112         isRequired = false,
113         defaultValue = DEFAULT_BUNDLE_PREFIX,
114         type = RestParameter.Type.STRING) },
115     responses = {
116       @RestResponse(description = "true/false", responseCode = HttpServletResponse.SC_OK),
117       @RestResponse(description = "cannot find any bundles with the given prefix", responseCode = HttpServletResponse.SC_NOT_FOUND) },
118     returnDescription = "The search results, expressed as xml or json.")
119   public Response checkBundles(@DefaultValue(DEFAULT_BUNDLE_PREFIX) @QueryParam("prefix") List<String> prefixes) {
120     return withBundles(prefixes, new Function<List<BundleInfo>, Response>() {
121       @Override
122       public Response apply(List<BundleInfo> infos) {
123         final String bundleVersion = infos.get(0).getBundleVersion();
124         final Option<String> buildNumber = infos.get(0).getBuildNumber();
125         for (BundleInfo a : infos) {
126           if (ne(a.getBundleVersion(), bundleVersion) || ne(a.getBuildNumber(), buildNumber))
127             return ok(TEXT_PLAIN_TYPE, "false");
128         }
129         return ok(TEXT_PLAIN_TYPE, "true");
130       }
131     });
132   }
133 
134   /** Return the common version of all bundles matching the given prefix. */
135   @GET
136   @Path("bundles/version")
137   @Produces(APPLICATION_JSON)
138   @RestQuery(
139     name = "bundleVersion",
140     description = "Return the common OSGi build version and build number of all bundles matching the given prefix.",
141     restParameters = {
142       @RestParameter(
143         name = "prefix",
144         description = "The bundle name prefixes to check. Defaults to '" + DEFAULT_BUNDLE_PREFIX + "'.",
145         isRequired = false,
146         defaultValue = DEFAULT_BUNDLE_PREFIX,
147         type = RestParameter.Type.STRING) },
148     responses = {
149       @RestResponse(description = "Version structure", responseCode = HttpServletResponse.SC_OK),
150       @RestResponse(description = "No bundles with the given prefix", responseCode = HttpServletResponse.SC_NOT_FOUND) },
151     returnDescription = "The search results as json.")
152   public Response getBundleVersion(@DefaultValue(DEFAULT_BUNDLE_PREFIX) @QueryParam("prefix") List<String> prefixes) {
153     return withBundles(prefixes, new Function<List<BundleInfo>, Response>() {
154       @Override
155       public Response apply(List<BundleInfo> infos) {
156         final Set<BundleVersion> versions = set();
157         for (BundleInfo bundle : infos) {
158           versions.add(bundle.getVersion());
159         }
160         final BundleInfo example = infos.get(0);
161         switch (versions.size()) {
162           case 0:
163             // no versions...
164             throw new Error("bug");
165           case 1:
166             // all versions align
167             return ok(obj(p("consistent", true))
168                 .append(fullVersionJson.apply(example.getVersion()))
169                 .append(obj(p("last-modified", lastModified))));
170           default:
171             // multiple versions found
172             return ok(obj(p("consistent", false),
173                           p("versions",
174                             arr(mlist(versions.iterator())
175                                     .map(Functions.<BundleVersion, Jsons.Val> co(fullVersionJson))))));
176         }
177       }
178     });
179   }
180 
181   @DELETE
182   @Path("bundles/host")
183   @RestQuery(
184           name = "clearHost",
185           description = "Removes the tracked bundles for a host. This is done automatically when you shut down "
186           + "Opencast. But this endpoint can be used to force this in case e.g. a machine got dropped. Make sure the "
187           + "host is actually gone! The database will be automatically rebuilt when Opencast on that host is "
188           + "(re)started.",
189           restParameters = {
190                   @RestParameter(
191                           name = "host",
192                           description = "The name of the host to clear",
193                           isRequired = true,
194                           type = RestParameter.Type.STRING,
195                           defaultValue = "") },
196           responses = {
197                   @RestResponse(description = "Version structure", responseCode = HttpServletResponse.SC_NO_CONTENT) },
198           returnDescription = "No data is returned.")
199   public Response clearHost(@QueryParam("host") String host) {
200     logger.debug("Removing tracked bundles of host: {}", host);
201     getDb().clear(host);
202     return Response.noContent().build();
203   }
204 
205   public static final Function<BundleVersion, Jsons.Obj> fullVersionJson = new Function<BundleVersion, Jsons.Obj>() {
206     @Override
207     public Jsons.Obj apply(BundleVersion version) {
208       return obj(p("version", version.getBundleVersion()), p("buildNumber", version.getBuildNumber().map(stringVal)));
209     }
210   };
211 
212   public static Jsons.Obj bundleInfoJson(BundleInfo bundle) {
213     return obj(p("host", bundle.getHost()), p("bundleSymbolicName", bundle.getBundleSymbolicName()),
214             p("bundleId", bundle.getBundleId())).append(fullVersionJson.apply(bundle.getVersion()));
215   }
216 
217   public static final Function<BundleInfo, Jsons.Obj> bundleInfo = new Function<BundleInfo, Jsons.Obj>() {
218     @Override
219     public Jsons.Obj apply(BundleInfo bundle) {
220       return bundleInfoJson(bundle);
221     }
222   };
223 
224   /** Run <code>f</code> if there is at least one bundle matching the given prefixes. */
225   private Response withBundles(List<String> prefixes, Function<List<BundleInfo>, Response> f) {
226     final List<BundleInfo> info = getDb().getBundles(toArray(String.class, prefixes));
227     if (info.size() > 0) {
228       return f.apply(info);
229     } else {
230       return notFound("No bundles match one of the given prefixes");
231     }
232   }
233 }