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 }