001 /*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * SonarQube is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
019 */
020 package org.sonar.api.server.ws;
021
022 import com.google.common.collect.ImmutableList;
023 import com.google.common.collect.ImmutableMap;
024 import com.google.common.collect.Maps;
025 import org.apache.commons.lang.StringUtils;
026 import org.sonar.api.ServerExtension;
027
028 import javax.annotation.CheckForNull;
029 import javax.annotation.Nullable;
030 import javax.annotation.concurrent.Immutable;
031 import java.util.Collection;
032 import java.util.List;
033 import java.util.Map;
034
035 /**
036 * Defines a web service. Note that contrary to the deprecated {@link org.sonar.api.web.Webservice}
037 * the ws is fully implemented in Java and does not require any Ruby on Rails code.
038 *
039 * <p/>
040 * The classes implementing this extension point must be declared in {@link org.sonar.api.SonarPlugin#getExtensions()}.
041 *
042 * <h3>How to use</h3>
043 * <pre>
044 * public class HelloWs implements WebService {
045 * {@literal @}Override
046 * public void define(Context context) {
047 * NewController controller = context.createController("api/hello");
048 * controller.setDescription("Web service example");
049 *
050 * // create the URL /api/hello/show
051 * controller.createAction("show")
052 * .setDescription("Entry point")
053 * .setHandler(new RequestHandler() {
054 * {@literal @}Override
055 * public void handle(Request request, Response response) {
056 * // read request parameters and generates response output
057 * response.newJsonWriter()
058 * .prop("hello", request.mandatoryParam("key"))
059 * .close();
060 * }
061 * })
062 * .createParam("key", "Example key");
063 *
064 * // important to apply changes
065 * controller.done();
066 * }
067 * }
068 * </pre>
069 * <h3>How to test</h3>
070 * <pre>
071 * public class HelloWsTest {
072 * WebService ws = new HelloWs();
073 *
074 * {@literal @}Test
075 * public void should_define_ws() throws Exception {
076 * // WsTester is available in the Maven artifact org.codehaus.sonar:sonar-testing-harness
077 * WsTester tester = new WsTester(ws);
078 * WebService.Controller controller = tester.controller("api/hello");
079 * assertThat(controller).isNotNull();
080 * assertThat(controller.path()).isEqualTo("api/hello");
081 * assertThat(controller.description()).isNotEmpty();
082 * assertThat(controller.actions()).hasSize(1);
083 *
084 * WebService.Action show = controller.action("show");
085 * assertThat(show).isNotNull();
086 * assertThat(show.key()).isEqualTo("show");
087 * assertThat(index.handler()).isNotNull();
088 * }
089 * }
090 * </pre>
091 *
092 * @since 4.2
093 */
094 public interface WebService extends ServerExtension {
095
096 class Context {
097 private final Map<String, Controller> controllers = Maps.newHashMap();
098
099 /**
100 * Create a new controller.
101 * <p/>
102 * Structure of request URL is <code>http://<server>/<>controller path>/<action path>?<parameters></code>.
103 *
104 * @param path the controller path must not start or end with "/". It is recommended to start with "api/"
105 * and to use lower-case format with underscores, for example "api/coding_rules". Usual actions
106 * are "list", "show", "create" and "delete"
107 */
108 public NewController createController(String path) {
109 return new NewController(this, path);
110 }
111
112 private void register(NewController newController) {
113 if (controllers.containsKey(newController.path)) {
114 throw new IllegalStateException(
115 String.format("The web service '%s' is defined multiple times", newController.path)
116 );
117 }
118 controllers.put(newController.path, new Controller(newController));
119 }
120
121 @CheckForNull
122 public Controller controller(String key) {
123 return controllers.get(key);
124 }
125
126 public List<Controller> controllers() {
127 return ImmutableList.copyOf(controllers.values());
128 }
129 }
130
131 class NewController {
132 private final Context context;
133 private final String path;
134 private String description, since;
135 private final Map<String, NewAction> actions = Maps.newHashMap();
136
137 private NewController(Context context, String path) {
138 if (StringUtils.isBlank(path)) {
139 throw new IllegalArgumentException("WS controller path must not be empty");
140 }
141 if (StringUtils.startsWith(path, "/") || StringUtils.endsWith(path, "/")) {
142 throw new IllegalArgumentException("WS controller path must not start or end with slash: " + path);
143 }
144 this.context = context;
145 this.path = path;
146 }
147
148 /**
149 * Important - this method must be called in order to apply changes and make the
150 * controller available in {@link org.sonar.api.server.ws.WebService.Context#controllers()}
151 */
152 public void done() {
153 context.register(this);
154 }
155
156 /**
157 * Optional plain-text description
158 */
159 public NewController setDescription(@Nullable String s) {
160 this.description = s;
161 return this;
162 }
163
164 /**
165 * Optional version when the controller was created
166 */
167 public NewController setSince(@Nullable String s) {
168 this.since = s;
169 return this;
170 }
171
172 public NewAction createAction(String actionKey) {
173 if (actions.containsKey(actionKey)) {
174 throw new IllegalStateException(
175 String.format("The action '%s' is defined multiple times in the web service '%s'", actionKey, path)
176 );
177 }
178 NewAction action = new NewAction(actionKey);
179 actions.put(actionKey, action);
180 return action;
181 }
182 }
183
184 @Immutable
185 class Controller {
186 private final String path, description, since;
187 private final Map<String, Action> actions;
188
189 private Controller(NewController newController) {
190 if (newController.actions.isEmpty()) {
191 throw new IllegalStateException(
192 String.format("At least one action must be declared in the web service '%s'", newController.path)
193 );
194 }
195 this.path = newController.path;
196 this.description = newController.description;
197 this.since = newController.since;
198 ImmutableMap.Builder<String, Action> mapBuilder = ImmutableMap.builder();
199 for (NewAction newAction : newController.actions.values()) {
200 mapBuilder.put(newAction.key, new Action(this, newAction));
201 }
202 this.actions = mapBuilder.build();
203 }
204
205 public String path() {
206 return path;
207 }
208
209 @CheckForNull
210 public String description() {
211 return description;
212 }
213
214 @CheckForNull
215 public String since() {
216 return since;
217 }
218
219 @CheckForNull
220 public Action action(String actionKey) {
221 return actions.get(actionKey);
222 }
223
224 public Collection<Action> actions() {
225 return actions.values();
226 }
227
228 /**
229 * Returns true if all the actions are for internal use
230 *
231 * @see org.sonar.api.server.ws.WebService.Action#isInternal()
232 * @since 4.3
233 */
234 public boolean isInternal() {
235 for (Action action : actions()) {
236 if (!action.isInternal()) {
237 return false;
238 }
239 }
240 return true;
241 }
242 }
243
244 class NewAction {
245 private final String key;
246 private String description, since;
247 private boolean post = false, isInternal = false;
248 private RequestHandler handler;
249 private Map<String, NewParam> newParams = Maps.newHashMap();
250
251 private NewAction(String key) {
252 this.key = key;
253 }
254
255 public NewAction setDescription(@Nullable String s) {
256 this.description = s;
257 return this;
258 }
259
260 public NewAction setSince(@Nullable String s) {
261 this.since = s;
262 return this;
263 }
264
265 public NewAction setPost(boolean b) {
266 this.post = b;
267 return this;
268 }
269
270 public NewAction setInternal(boolean b) {
271 this.isInternal = b;
272 return this;
273 }
274
275 public NewAction setHandler(RequestHandler h) {
276 this.handler = h;
277 return this;
278 }
279
280 public NewParam createParam(String paramKey) {
281 if (newParams.containsKey(paramKey)) {
282 throw new IllegalStateException(
283 String.format("The parameter '%s' is defined multiple times in the action '%s'", paramKey, key)
284 );
285 }
286 NewParam newParam = new NewParam(paramKey);
287 newParams.put(paramKey, newParam);
288 return newParam;
289 }
290
291 public NewAction createParam(String paramKey, @Nullable String description) {
292 createParam(paramKey).setDescription(description);
293 return this;
294 }
295 }
296
297 @Immutable
298 class Action {
299 private final String key, path, description, since;
300 private final boolean post, isInternal;
301 private final RequestHandler handler;
302 private final Map<String, Param> params;
303
304 private Action(Controller controller, NewAction newAction) {
305 this.key = newAction.key;
306 this.path = String.format("%s/%s", controller.path(), key);
307 this.description = newAction.description;
308 this.since = StringUtils.defaultIfBlank(newAction.since, controller.since);
309 this.post = newAction.post;
310 this.isInternal = newAction.isInternal;
311
312 if (newAction.handler == null) {
313 throw new IllegalStateException("RequestHandler is not set on action " + path);
314 }
315 this.handler = newAction.handler;
316
317 ImmutableMap.Builder<String, Param> mapBuilder = ImmutableMap.builder();
318 for (NewParam newParam : newAction.newParams.values()) {
319 mapBuilder.put(newParam.key, new Param(newParam));
320 }
321 this.params = mapBuilder.build();
322 }
323
324 public String key() {
325 return key;
326 }
327
328 public String path() {
329 return path;
330 }
331
332 @CheckForNull
333 public String description() {
334 return description;
335 }
336
337 /**
338 * Set if different than controller.
339 */
340 @CheckForNull
341 public String since() {
342 return since;
343 }
344
345 public boolean isPost() {
346 return post;
347 }
348
349 public boolean isInternal() {
350 return isInternal;
351 }
352
353 public RequestHandler handler() {
354 return handler;
355 }
356
357 @CheckForNull
358 public Param param(String key) {
359 return params.get(key);
360 }
361
362 public Collection<Param> params() {
363 return params.values();
364 }
365
366 @Override
367 public String toString() {
368 return path;
369 }
370 }
371
372 class NewParam {
373 private String key, description;
374
375 private NewParam(String key) {
376 this.key = key;
377 }
378
379 public NewParam setDescription(@Nullable String s) {
380 this.description = s;
381 return this;
382 }
383
384 @Override
385 public String toString() {
386 return key;
387 }
388 }
389
390 @Immutable
391 class Param {
392 private final String key, description;
393
394 public Param(NewParam newParam) {
395 this.key = newParam.key;
396 this.description = newParam.description;
397 }
398
399 public String key() {
400 return key;
401 }
402
403 @CheckForNull
404 public String description() {
405 return description;
406 }
407
408 @Override
409 public String toString() {
410 return key;
411 }
412 }
413
414 /**
415 * Executed once at server startup.
416 */
417 void define(Context context);
418
419 }