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.adminui.tobira;
23  
24  import org.json.simple.JSONArray;
25  import org.json.simple.JSONObject;
26  import org.json.simple.parser.JSONParser;
27  import org.json.simple.parser.ParseException;
28  import org.slf4j.Logger;
29  import org.slf4j.LoggerFactory;
30  
31  import java.io.IOException;
32  import java.net.URI;
33  import java.net.URISyntaxException;
34  import java.net.http.HttpClient;
35  import java.net.http.HttpRequest;
36  import java.net.http.HttpResponse;
37  import java.util.HashMap;
38  import java.util.List;
39  import java.util.Map;
40  
41  public final class TobiraService {
42  
43    public JSONObject getPage(String path) throws TobiraException {
44      return request(
45              "query AdminUiPage($path: String!) {"
46                      + "  page: realmByPath(path: $path) {"
47                      + "    ... RealmData"
48                      + "    children {"
49                      + "      ... RealmData"
50                      + "      blocks {"
51                      // We only need the number of blocks, but we need to query something from them, unfortunately.
52                      + "        id"
53                      + "      }"
54                      + "    }"
55                      + "  }"
56                      + "}"
57                      + "fragment RealmData on Realm {"
58                      + "  title: name"
59                      + "  segment: pathSegment"
60                      + "  path"
61                      + "}",
62              Map.of("path", path));
63    }
64  
65    public JSONObject getHostPages(String seriesId) throws TobiraException {
66      return (JSONObject) request(
67              "query AdminUIHostPages($seriesId: String!) {"
68                      + "  series: seriesByOpencastId(id: $seriesId) {"
69                      + "    id"
70                      + "    hostPages: hostRealms {"
71                      + "      ... RealmData"
72                      + "      blocks { id }"
73                      + "      ancestors {"
74                      + "        ... RealmData"
75                      + "      }"
76                      + "    }"
77                      + "  }"
78                      + "}"
79                      + "fragment RealmData on Realm {"
80                      + "  title: name"
81                      + "  segment: pathSegment"
82                      + "  path"
83                      + "}",
84              Map.of("seriesId", seriesId))
85              .get("series");
86    }
87  
88    public JSONObject getEventHostPages(String eventId) throws TobiraException {
89      return (JSONObject) request(
90              "query AdminUIEventHostPages($eventId: String!) {"
91                      + "  event: eventByOpencastId(id: $eventId) {"
92                      + "    ...on AuthorizedEvent {"
93                      + "      id"
94                      + "      hostPages: hostRealms {"
95                      + "        title: name"
96                      + "        path"
97                      + "        ancestors { title: name }"
98                      + "      }"
99                      + "    }"
100                     + "  }"
101                     + "}",
102             Map.of("eventId", eventId))
103             .get("event");
104   }
105 
106   public void mount(Map<String, Object> variables) throws TobiraException {
107     request(
108             "mutation AdminUIMountSeries($series: NewSeries!, $parentPagePath: String!, $newPages: [RealmSpecifier!]!) {"
109                     + "  mountSeries(series: $series, parentRealmPath: $parentPagePath, newRealms: $newPages) {"
110                     // We need to query something here, but we really don't care at the moment
111                     + "    id"
112                     + "  }"
113                     + "}",
114             variables);
115   }
116 
117   public Integer createRealmLineage(List<JSONObject> pathComponents) throws TobiraException {
118     return (Integer) request(
119             "mutation AdminUICreateRealmLineage($realms: [RealmLineageComponent!]!) {"
120                     + "  createRealmLineage(realms: $realms) { numCreated }"
121                     + "}",
122             Map.of("realms", pathComponents))
123             .get("numCreated");
124   }
125 
126   public String addSeriesMountPoint(Map<String, Object> variables) throws TobiraException {
127     return (String) request(
128             "mutation AdminUIAddSeriesMountPoint($seriesId: String!, $targetPath: String!) {"
129                     + "  addSeriesMountPoint(seriesOcId: $seriesId, targetPath: $targetPath) {"
130                     + "    id"
131                     + "  }"
132                     + "}",
133             variables)
134             .get("id");
135   }
136 
137   public JSONObject removeSeriesMountPoint(Map<String, Object> variables) throws TobiraException {
138     return (JSONObject) request(
139             "mutation AdminUIRemoveSeriesMountPoint($seriesId: String!, $currentPath: String!) {"
140                     + "  outcome: removeSeriesMountPoint(seriesOcId: $seriesId, path: $currentPath) {"
141                     + "    __typename"
142                     + "  }"
143                     + "}",
144             variables)
145             .get("outcome");
146   }
147 
148   public boolean ready() {
149     return this.endpoint != null && this.trustedKey != null;
150   }
151 
152   private JSONObject request(String query, Map<String, Object> variables) throws TobiraException {
153 
154     var queryObject = new JSONObject(Map.of(
155             "query", query,
156             "variables", new JSONObject(variables)));
157 
158     var request = HttpRequest.newBuilder()
159             .uri(endpoint)
160             .header("content-type", "application/json")
161             .header("x-tobira-trusted-external-key", trustedKey)
162             .POST(HttpRequest.BodyPublishers.ofString(queryObject.toJSONString()))
163             .build();
164 
165     try {
166       var response = client.send(request, HttpResponse.BodyHandlers.ofString());
167       if (response.statusCode() != 200) {
168         throw new TobiraException(response);
169       }
170       var responseObject = (JSONObject) new JSONParser().parse(response.body());
171       var errors = (JSONArray) responseObject.get("errors");
172       if (errors != null) {
173         throw new TobiraException(response, errors);
174       }
175       return (JSONObject) responseObject.get("data");
176     } catch (IOException | InterruptedException | ParseException e) {
177       throw new TobiraException(e);
178     }
179   }
180 
181   public String getOrigin() {
182     return origin;
183   }
184 
185   public void setOrigin(String origin) {
186     this.origin = origin;
187     if (origin == null) {
188       this.endpoint = null;
189     } else {
190       try {
191         this.endpoint = new URI(origin).resolve("/graphql");
192       } catch (URISyntaxException e) {
193         logger.error("Invalid Tobira origin {}", origin, e);
194         this.origin = null;
195         this.endpoint = null;
196       }
197     }
198   }
199 
200   public void setTrustedKey(String trustedKey) {
201     this.trustedKey = trustedKey;
202   }
203 
204   private String origin;
205   private URI endpoint;
206   private String trustedKey;
207 
208   private static final HttpClient client = HttpClient.newBuilder()
209           .followRedirects(HttpClient.Redirect.NORMAL)
210           .build();
211 
212   private static final Logger logger = LoggerFactory.getLogger(TobiraService.class);
213 
214   private static Map<String, TobiraService> tobiras = new HashMap<>();
215 
216   public static TobiraService getTobira(String organization) {
217     return tobiras.computeIfAbsent(organization, org -> new TobiraService());
218   }
219 }