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://&lt;server&gt;/&lt>controller path&gt;/&lt;action path&gt;?&lt;parameters&gt;</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    }