001/* 002 * SonarQube, open source software quality management tool. 003 * Copyright (C) 2008-2013 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 */ 020package org.sonar.api.server.ws; 021 022import com.google.common.collect.ImmutableList; 023import com.google.common.collect.ImmutableMap; 024import com.google.common.collect.Maps; 025import org.apache.commons.lang.StringUtils; 026import org.sonar.api.ServerExtension; 027 028import javax.annotation.CheckForNull; 029import javax.annotation.Nullable; 030import javax.annotation.concurrent.Immutable; 031import java.util.Collection; 032import java.util.List; 033import 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 * <h2>How to use</h2> 043 * <pre> 044 * public class HelloWs implements WebService { 045 * @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 * @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 * <h2>How to unit test</h2> 070 * <pre> 071 * public class HelloWsTest { 072 * WebService ws = new HelloWs(); 073 * 074 * @Test 075 * public void should_define_ws() throws Exception { 076 * // WsTester is available in the Maven artifact org.codehaus.sonar:sonar-plugin-api 077 * // with type "test-jar" 078 * WsTester tester = new WsTester(ws); 079 * WebService.Controller controller = tester.controller("api/hello"); 080 * assertThat(controller).isNotNull(); 081 * assertThat(controller.path()).isEqualTo("api/hello"); 082 * assertThat(controller.description()).isNotEmpty(); 083 * assertThat(controller.actions()).hasSize(1); 084 * 085 * WebService.Action show = controller.action("show"); 086 * assertThat(show).isNotNull(); 087 * assertThat(show.key()).isEqualTo("show"); 088 * assertThat(index.handler()).isNotNull(); 089 * } 090 * } 091 * </pre> 092 * 093 * @since 4.2 094 */ 095public interface WebService extends ServerExtension { 096 097 class Context { 098 private final Map<String, Controller> controllers = Maps.newHashMap(); 099 100 /** 101 * Create a new controller. 102 * <p/> 103 * Structure of request URL is <code>http://<server>/<>controller path>/<action path>?<parameters></code>. 104 * 105 * @param path the controller path must not start or end with "/". It is recommended to start with "api/" 106 */ 107 public NewController createController(String path) { 108 return new NewController(this, path); 109 } 110 111 private void register(NewController newController) { 112 if (controllers.containsKey(newController.path)) { 113 throw new IllegalStateException( 114 String.format("The web service '%s' is defined multiple times", newController.path) 115 ); 116 } 117 controllers.put(newController.path, new Controller(newController)); 118 } 119 120 @CheckForNull 121 public Controller controller(String key) { 122 return controllers.get(key); 123 } 124 125 public List<Controller> controllers() { 126 return ImmutableList.copyOf(controllers.values()); 127 } 128 } 129 130 class NewController { 131 private final Context context; 132 private final String path; 133 private String description, since; 134 private final Map<String, NewAction> actions = Maps.newHashMap(); 135 136 private NewController(Context context, String path) { 137 if (StringUtils.isBlank(path)) { 138 throw new IllegalArgumentException("WS controller path must not be empty"); 139 } 140 if (StringUtils.startsWith(path, "/") || StringUtils.endsWith(path, "/")) { 141 throw new IllegalArgumentException("WS controller path must not start or end with slash: " + path); 142 } 143 this.context = context; 144 this.path = path; 145 } 146 147 /** 148 * Important - this method must be called in order to apply changes and make the 149 * controller available in {@link org.sonar.api.server.ws.WebService.Context#controllers()} 150 */ 151 public void done() { 152 context.register(this); 153 } 154 155 /** 156 * Optional plain-text description 157 */ 158 public NewController setDescription(@Nullable String s) { 159 this.description = s; 160 return this; 161 } 162 163 /** 164 * Optional version when the controller was created 165 */ 166 public NewController setSince(@Nullable String s) { 167 this.since = s; 168 return this; 169 } 170 171 public NewAction createAction(String actionKey) { 172 if (actions.containsKey(actionKey)) { 173 throw new IllegalStateException( 174 String.format("The action '%s' is defined multiple times in the web service '%s'", actionKey, path) 175 ); 176 } 177 NewAction action = new NewAction(actionKey); 178 actions.put(actionKey, action); 179 return action; 180 } 181 } 182 183 @Immutable 184 class Controller { 185 private final String path, description, since; 186 private final Map<String, Action> actions; 187 188 private Controller(NewController newController) { 189 if (newController.actions.isEmpty()) { 190 throw new IllegalStateException( 191 String.format("At least one action must be declared in the web service '%s'", newController.path) 192 ); 193 } 194 this.path = newController.path; 195 this.description = newController.description; 196 this.since = newController.since; 197 ImmutableMap.Builder<String, Action> mapBuilder = ImmutableMap.builder(); 198 for (NewAction newAction : newController.actions.values()) { 199 mapBuilder.put(newAction.key, new Action(this, newAction)); 200 } 201 this.actions = mapBuilder.build(); 202 } 203 204 public String path() { 205 return path; 206 } 207 208 @CheckForNull 209 public String description() { 210 return description; 211 } 212 213 @CheckForNull 214 public String since() { 215 return since; 216 } 217 218 @CheckForNull 219 public Action action(String actionKey) { 220 return actions.get(actionKey); 221 } 222 223 public Collection<Action> actions() { 224 return actions.values(); 225 } 226 } 227 228 class NewAction { 229 private final String key; 230 private String description, since; 231 private boolean post = false, isInternal = false; 232 private RequestHandler handler; 233 private Map<String, NewParam> newParams = Maps.newHashMap(); 234 235 private NewAction(String key) { 236 this.key = key; 237 } 238 239 public NewAction setDescription(@Nullable String s) { 240 this.description = s; 241 return this; 242 } 243 244 public NewAction setSince(@Nullable String s) { 245 this.since = s; 246 return this; 247 } 248 249 public NewAction setPost(boolean b) { 250 this.post = b; 251 return this; 252 } 253 254 public NewAction setInternal(boolean b) { 255 this.isInternal = b; 256 return this; 257 } 258 259 public NewAction setHandler(RequestHandler h) { 260 this.handler = h; 261 return this; 262 } 263 264 public NewParam createParam(String paramKey) { 265 if (newParams.containsKey(paramKey)) { 266 throw new IllegalStateException( 267 String.format("The parameter '%s' is defined multiple times in the action '%s'", paramKey, key) 268 ); 269 } 270 NewParam newParam = new NewParam(paramKey); 271 newParams.put(paramKey, newParam); 272 return newParam; 273 } 274 275 public NewAction createParam(String paramKey, @Nullable String description) { 276 createParam(paramKey).setDescription(description); 277 return this; 278 } 279 } 280 281 @Immutable 282 class Action { 283 private final String key, path, description, since; 284 private final boolean post, isInternal; 285 private final RequestHandler handler; 286 private final Map<String, Param> params; 287 288 private Action(Controller controller, NewAction newAction) { 289 this.key = newAction.key; 290 this.path = String.format("%s/%s", controller.path(), key); 291 this.description = newAction.description; 292 this.since = StringUtils.defaultIfBlank(newAction.since, controller.since); 293 this.post = newAction.post; 294 this.isInternal = newAction.isInternal; 295 296 if (newAction.handler == null) { 297 throw new IllegalStateException("RequestHandler is not set on action " + path); 298 } 299 this.handler = newAction.handler; 300 301 ImmutableMap.Builder<String, Param> mapBuilder = ImmutableMap.builder(); 302 for (NewParam newParam : newAction.newParams.values()) { 303 mapBuilder.put(newParam.key, new Param(newParam)); 304 } 305 this.params = mapBuilder.build(); 306 } 307 308 public String key() { 309 return key; 310 } 311 312 public String path() { 313 return path; 314 } 315 316 @CheckForNull 317 public String description() { 318 return description; 319 } 320 321 /** 322 * Set if different than controller. 323 */ 324 @CheckForNull 325 public String since() { 326 return since; 327 } 328 329 public boolean isPost() { 330 return post; 331 } 332 333 public boolean isInternal() { 334 return isInternal; 335 } 336 337 public RequestHandler handler() { 338 return handler; 339 } 340 341 @CheckForNull 342 public Param param(String key) { 343 return params.get(key); 344 } 345 346 public Collection<Param> params() { 347 return params.values(); 348 } 349 350 @Override 351 public String toString() { 352 return path; 353 } 354 } 355 356 class NewParam { 357 private String key, description; 358 359 private NewParam(String key) { 360 this.key = key; 361 } 362 363 public NewParam setDescription(@Nullable String s) { 364 this.description = s; 365 return this; 366 } 367 368 @Override 369 public String toString() { 370 return key; 371 } 372 } 373 374 @Immutable 375 class Param { 376 private final String key, description; 377 378 public Param(NewParam newParam) { 379 this.key = newParam.key; 380 this.description = newParam.description; 381 } 382 383 public String key() { 384 return key; 385 } 386 387 @CheckForNull 388 public String description() { 389 return description; 390 } 391 392 @Override 393 public String toString() { 394 return key; 395 } 396 } 397 398 /** 399 * Executed once at server startup. 400 */ 401 void define(Context context); 402 403}