]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8464 Create WS api/project_analyses/create_event 1435/head
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Tue, 6 Dec 2016 17:34:44 +0000 (18:34 +0100)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Wed, 7 Dec 2016 16:23:30 +0000 (17:23 +0100)
31 files changed:
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/CreateEventAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/ProjectAnalysesWs.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/ProjectAnalysesWsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/create_event-example.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/CreateEventActionTest.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/component/SnapshotDao.java
sonar-db/src/main/java/org/sonar/db/component/SnapshotMapper.java
sonar-db/src/main/java/org/sonar/db/event/EventDao.java
sonar-db/src/main/java/org/sonar/db/event/EventDto.java
sonar-db/src/main/java/org/sonar/db/event/EventMapper.java
sonar-db/src/main/java/org/sonar/db/event/EventValidator.java [new file with mode: 0644]
sonar-db/src/main/resources/org/sonar/db/component/SnapshotMapper.xml
sonar-db/src/main/resources/org/sonar/db/event/EventMapper.xml
sonar-db/src/test/java/org/sonar/db/DbTester.java
sonar-db/src/test/java/org/sonar/db/component/SnapshotDaoTest.java
sonar-db/src/test/java/org/sonar/db/event/EventDaoTest.java
sonar-db/src/test/java/org/sonar/db/event/EventDbTester.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/event/EventTesting.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/event/EventValidatorTest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/CreateEventRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/ProjectAnalysesWsParameters.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/package-info.java [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-projectanalyses.proto [new file with mode: 0644]
sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/CreateEventRequestTest.java [new file with mode: 0644]
sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/ProjectAnalysesWsParametersTest.java [new file with mode: 0644]

index dbe62325ffaadccc42ef679c386369e848b518b6..e55526c421883067acf40739b41ae9a6280c863b 100644 (file)
@@ -121,9 +121,9 @@ public class ComponentTreeAction implements MeasuresWsAction {
       .setDescription(format("Navigate through components based on the chosen strategy with specified measures. The %s or the %s parameter must be provided.<br>" +
         "Requires one of the following permissions:" +
         "<ul>" +
-        "<li>'Administer System'</li>" +
-        "<li>'Administer' rights on the specified project</li>" +
-        "<li>'Browse' on the specified project</li>" +
+        "  <li>'Administer System'</li>" +
+        "  <li>'Administer' rights on the specified project</li>" +
+        "  <li>'Browse' on the specified project</li>" +
         "</ul>" +
         "When limiting search with the %s parameter, directories are not returned.",
         PARAM_BASE_COMPONENT_ID, PARAM_BASE_COMPONENT_KEY, Param.TEXT_QUERY))
index 5613a5ff655cb8a2bd6e8aa98a5ecad44e547e0e..3255dc6f23dfaf2010d26510d33240f50df3e1f6 100644 (file)
@@ -148,6 +148,7 @@ import org.sonar.server.plugins.ws.PluginsWs;
 import org.sonar.server.plugins.ws.UninstallAction;
 import org.sonar.server.plugins.ws.UpdatesAction;
 import org.sonar.server.project.ws.ProjectsWsModule;
+import org.sonar.server.projectanalysis.ProjectAnalysisModule;
 import org.sonar.server.projectlink.ws.ProjectLinksModule;
 import org.sonar.server.property.InternalPropertiesImpl;
 import org.sonar.server.qualitygate.QualityGateModule;
@@ -490,6 +491,9 @@ public class PlatformLevel4 extends PlatformLevel {
       // Project Links
       ProjectLinksModule.class,
 
+      // Project Analyses
+      ProjectAnalysisModule.class,
+
       // System
       ServerLogging.class,
       RestartAction.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java
new file mode 100644 (file)
index 0000000..0c98286
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.projectanalysis;
+
+import org.sonar.core.platform.Module;
+import org.sonar.server.projectanalysis.ws.CreateEventAction;
+import org.sonar.server.projectanalysis.ws.ProjectAnalysesWs;
+
+public class ProjectAnalysisModule extends Module {
+
+  @Override
+  protected void configureModule() {
+    add(
+      ProjectAnalysesWs.class,
+      // actions
+      CreateEventAction.class);
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/package-info.java
new file mode 100644 (file)
index 0000000..950feac
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonar.server.projectanalysis;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/CreateEventAction.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/CreateEventAction.java
new file mode 100644 (file)
index 0000000..d2ea1f8
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.projectanalysis.ws;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.Uuids;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.event.EventDto;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.ProjectAnalyses.CreateEventResponse;
+import org.sonarqube.ws.ProjectAnalyses.Event;
+import org.sonarqube.ws.client.projectanalysis.CreateEventRequest;
+import org.sonarqube.ws.client.projectanalysis.CreateEventRequest.Category;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+import static org.sonarqube.ws.client.projectanalysis.CreateEventRequest.Category.OTHER;
+import static org.sonarqube.ws.client.projectanalysis.CreateEventRequest.Category.VERSION;
+import static org.sonarqube.ws.client.projectanalysis.CreateEventRequest.Category.fromLabel;
+import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_ANALYSIS;
+import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_CATEGORY;
+import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_DESCRIPTION;
+import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_NAME;
+
+public class CreateEventAction implements ProjectAnalysesWsAction {
+  private final DbClient dbClient;
+  private final UuidFactory uuidFactory;
+  private final System2 system;
+  private final UserSession userSession;
+
+  public CreateEventAction(DbClient dbClient, UuidFactory uuidFactory, System2 system, UserSession userSession) {
+    this.dbClient = dbClient;
+    this.uuidFactory = uuidFactory;
+    this.system = system;
+    this.userSession = userSession;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction("create_event")
+      .setDescription("Add an event to a project analysis.<br>" +
+        "Requires one of the following permissions:" +
+        "<ul>" +
+        "  <li>'Administer System'</li>" +
+        "  <li>'Administer' rights on the specified project</li>" +
+        "</ul>")
+      .setSince("6.3")
+      .setPost(true)
+      .setResponseExample(getClass().getResource("create_event-example.json"))
+      .setHandler(this);
+
+    action.createParam(PARAM_ANALYSIS)
+      .setDescription("Analysis key")
+      .setExampleValue(Uuids.UUID_EXAMPLE_01)
+      .setRequired(true);
+
+    action.createParam(PARAM_CATEGORY)
+      .setDescription("Category")
+      .setDefaultValue(OTHER)
+      .setPossibleValues(VERSION, OTHER);
+
+    action.createParam(PARAM_NAME)
+      .setDescription("Name")
+      .setExampleValue("5.6")
+      .setRequired(true);
+
+    action.createParam(PARAM_DESCRIPTION)
+      .setDescription("Description")
+      .setExampleValue("Version released");
+  }
+
+  @Override
+  public void handle(Request httpRequest, Response httpResponse) throws Exception {
+    CreateEventRequest request = toAddEventRequest(httpRequest);
+    CreateEventResponse response = doHandle(request);
+
+    writeProtobuf(response, httpRequest, httpResponse);
+  }
+
+  private CreateEventResponse doHandle(CreateEventRequest request) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      SnapshotDto analysis = getAnalysis(dbSession, request);
+      checkPermissions(analysis);
+      checkExistingDbEvents(dbSession, request, analysis);
+      EventDto dbEvent = insertDbEvent(dbSession, request, analysis);
+      return toCreateEventResponse(dbEvent);
+    }
+  }
+
+  private void checkPermissions(SnapshotDto analysis) {
+    userSession.checkComponentUuidPermission(UserRole.ADMIN, analysis.getComponentUuid());
+  }
+
+  private EventDto insertDbEvent(DbSession dbSession, CreateEventRequest request, SnapshotDto analysis) {
+    EventDto dbEvent = dbClient.eventDao().insert(dbSession, toDbEvent(request, analysis));
+    if (VERSION.equals(request.getCategory())) {
+      dbClient.snapshotDao().updateVersion(dbSession, analysis.getUuid(), request.getName());
+    }
+    dbSession.commit();
+    return dbEvent;
+  }
+
+  private SnapshotDto getAnalysis(DbSession dbSession, CreateEventRequest request) {
+    SnapshotDto analysis = dbClient.snapshotDao().selectByUuid(dbSession, request.getAnalysis())
+      .orElseThrow(() -> new NotFoundException(format("Analysis '%s' is not found", request.getAnalysis())));
+    ComponentDto project = dbClient.componentDao().selectByUuid(dbSession, analysis.getComponentUuid()).orNull();
+    checkState(project != null, "Project of analysis '%s' is not found", analysis.getUuid());
+    checkArgument(Qualifiers.PROJECT.equals(project.qualifier()) && Scopes.PROJECT.equals(project.scope()),
+      "An event must be created on a project");
+    return analysis;
+  }
+
+  private static CreateEventRequest toAddEventRequest(Request request) {
+    return CreateEventRequest.builder()
+      .setAnalysis(request.mandatoryParam(PARAM_ANALYSIS))
+      .setName(request.mandatoryParam(PARAM_NAME))
+      .setCategory(request.mandatoryParamAsEnum(PARAM_CATEGORY, Category.class))
+      .setDescription(request.param(PARAM_DESCRIPTION))
+      .build();
+  }
+
+  private EventDto toDbEvent(CreateEventRequest request, SnapshotDto analysis) {
+    return new EventDto()
+      .setUuid(uuidFactory.create())
+      .setAnalysisUuid(analysis.getUuid())
+      .setComponentUuid(analysis.getComponentUuid())
+      .setCategory(request.getCategory().getLabel())
+      .setName(request.getName())
+      .setDescription(request.getDescription())
+      .setCreatedAt(system.now())
+      .setDate(analysis.getCreatedAt());
+  }
+
+  private static CreateEventResponse toCreateEventResponse(EventDto dbEvent) {
+    Event.Builder wsEvent = Event.newBuilder()
+      .setKey(dbEvent.getUuid())
+      .setCategory(fromLabel(dbEvent.getCategory()).name())
+      .setAnalysis(dbEvent.getAnalysisUuid())
+      .setName(dbEvent.getName());
+    setNullable(dbEvent.getDescription(), wsEvent::setDescription);
+
+    return CreateEventResponse.newBuilder().setEvent(wsEvent).build();
+  }
+
+  private void checkExistingDbEvents(DbSession dbSession, CreateEventRequest request, SnapshotDto analysis) {
+    List<EventDto> dbEvents = dbClient.eventDao().selectByAnalysisUuid(dbSession, analysis.getUuid());
+    Predicate<EventDto> similarEventExisting = filterSimilarEvents(request);
+    dbEvents.stream()
+      .filter(similarEventExisting)
+      .findAny()
+      .ifPresent(throwException(request));
+  }
+
+  private static Predicate<EventDto> filterSimilarEvents(CreateEventRequest request) {
+    switch (request.getCategory()) {
+      case VERSION:
+        return dbEvent -> VERSION.getLabel().equals(dbEvent.getCategory());
+      case OTHER:
+        return dbEvent -> OTHER.getLabel().equals(dbEvent.getCategory()) && request.getName().equals(dbEvent.getName());
+      default:
+        throw new IllegalStateException("Event category not handled: " + request.getCategory());
+    }
+  }
+
+  private static Consumer<EventDto> throwException(CreateEventRequest request) {
+    switch (request.getCategory()) {
+      case VERSION:
+        return dbEvent -> {
+          throw new IllegalArgumentException(format("A version event already exists on analysis '%s'", request.getAnalysis()));
+        };
+      case OTHER:
+        return dbEvent -> {
+          throw new IllegalArgumentException(format("An 'other' event with the same name already exists on analysis '%s'", request.getAnalysis()));
+        };
+      default:
+        throw new IllegalStateException("Event category not handled: " + request.getCategory());
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/ProjectAnalysesWs.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/ProjectAnalysesWs.java
new file mode 100644 (file)
index 0000000..2e878db
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.projectanalysis.ws;
+
+import org.sonar.api.server.ws.WebService;
+
+public class ProjectAnalysesWs implements WebService {
+
+  private final ProjectAnalysesWsAction[] actions;
+
+  public ProjectAnalysesWs(ProjectAnalysesWsAction... actions) {
+    this.actions = actions;
+  }
+
+  @Override
+  public void define(Context context) {
+    NewController controller = context.createController("api/project_analyses")
+      .setDescription("Manage project analyses.")
+      .setSince("6.3");
+
+    for (ProjectAnalysesWsAction action : actions) {
+      action.define(controller);
+    }
+
+    controller.done();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/ProjectAnalysesWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/ProjectAnalysesWsAction.java
new file mode 100644 (file)
index 0000000..4b14578
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.projectanalysis.ws;
+
+import org.sonar.server.ws.WsAction;
+
+public interface ProjectAnalysesWsAction extends WsAction {
+  // Marker interface
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/package-info.java
new file mode 100644 (file)
index 0000000..c4063ae
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonar.server.projectanalysis.ws;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/create_event-example.json b/server/sonar-server/src/main/resources/org/sonar/server/projectanalysis/ws/create_event-example.json
new file mode 100644 (file)
index 0000000..14e486d
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "event": {
+    "analysis": "A2",
+    "key": "E1",
+    "category": "OTHER",
+    "name": "My Custom Event",
+    "description": "event description"
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java
new file mode 100644 (file)
index 0000000..40d45b0
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.projectanalysis;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProjectAnalysisModuleTest {
+
+  @Test
+  public void verify_count_of_added_components() {
+    ComponentContainer container = new ComponentContainer();
+    new ProjectAnalysisModule().configure(container);
+    assertThat(container.size()).isEqualTo(2 + 2);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/CreateEventActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/CreateEventActionTest.java
new file mode 100644 (file)
index 0000000..1074e51
--- /dev/null
@@ -0,0 +1,351 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.projectanalysis.ws;
+
+import com.google.common.base.Throwables;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.permission.GlobalPermissions;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.component.SnapshotTesting;
+import org.sonar.db.event.EventDto;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.ProjectAnalyses;
+import org.sonarqube.ws.ProjectAnalyses.CreateEventResponse;
+import org.sonarqube.ws.client.projectanalysis.CreateEventRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.component.ComponentTesting.newProjectDto;
+import static org.sonar.db.component.ComponentTesting.newView;
+import static org.sonar.db.component.SnapshotTesting.newAnalysis;
+import static org.sonar.db.component.SnapshotTesting.newSnapshot;
+import static org.sonar.test.JsonAssert.assertJson;
+import static org.sonarqube.ws.client.WsRequest.Method.POST;
+import static org.sonarqube.ws.client.projectanalysis.CreateEventRequest.Category.OTHER;
+import static org.sonarqube.ws.client.projectanalysis.CreateEventRequest.Category.VERSION;
+import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_ANALYSIS;
+import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_CATEGORY;
+import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_DESCRIPTION;
+import static org.sonarqube.ws.client.projectanalysis.ProjectAnalysesWsParameters.PARAM_NAME;
+
+public class CreateEventActionTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone().setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN);
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+  private DbClient dbClient = db.getDbClient();
+  private DbSession dbSession = db.getSession();
+
+  private UuidFactory uuidFactory = UuidFactoryFast.getInstance();
+  private System2 system = mock(System2.class);
+
+  private WsActionTester ws = new WsActionTester(new CreateEventAction(dbClient, uuidFactory, system, userSession));
+
+  @Before
+  public void setUp() {
+    when(system.now()).thenReturn(42L);
+  }
+
+  @Test
+  public void json_example() {
+    ComponentDto project = db.components().insertProject();
+    SnapshotDto analysis = dbClient.snapshotDao().insert(dbSession, SnapshotTesting.newAnalysis(project).setUuid("A2"));
+    db.commit();
+    uuidFactory = mock(UuidFactory.class);
+    when(uuidFactory.create()).thenReturn("E1");
+    ws = new WsActionTester(new CreateEventAction(dbClient, uuidFactory, system, userSession));
+
+    String result = ws.newRequest()
+      .setParam(PARAM_ANALYSIS, analysis.getUuid())
+      .setParam(PARAM_CATEGORY, OTHER.name())
+      .setParam(PARAM_NAME, "My Custom Event")
+      .setParam(PARAM_DESCRIPTION, "event description")
+      .execute().getInput();
+
+    assertJson(result).isSimilarTo(getClass().getResource("create_event-example.json"));
+  }
+
+  @Test
+  public void create_event_in_db() {
+    SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto());
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setCategory(VERSION)
+      .setName("5.6.3")
+      .setDescription("LTS version");
+    when(system.now()).thenReturn(123_456_789L);
+
+    CreateEventResponse result = call(request);
+
+    List<EventDto> dbEvents = dbClient.eventDao().selectByComponentUuid(dbSession, analysis.getComponentUuid());
+    assertThat(dbEvents).hasSize(1);
+    EventDto dbEvent = dbEvents.get(0);
+    assertThat(dbEvent.getName()).isEqualTo("5.6.3");
+    assertThat(dbEvent.getCategory()).isEqualTo(VERSION.getLabel());
+    assertThat(dbEvent.getDescription()).isEqualTo("LTS version");
+    assertThat(dbEvent.getAnalysisUuid()).isEqualTo(analysis.getUuid());
+    assertThat(dbEvent.getComponentUuid()).isEqualTo(analysis.getComponentUuid());
+    assertThat(dbEvent.getUuid()).isEqualTo(result.getEvent().getKey());
+    assertThat(dbEvent.getCreatedAt()).isEqualTo(123_456_789L);
+    assertThat(dbEvent.getDate()).isEqualTo(analysis.getCreatedAt());
+  }
+
+  @Test
+  public void create_event_as_project_admin() {
+    SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto("P1"));
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setCategory(VERSION)
+      .setName("5.6.3")
+      .setDescription("LTS version");
+    userSession.anonymous().addProjectUuidPermissions(UserRole.ADMIN, "P1");
+
+    CreateEventResponse result = call(request);
+
+    assertThat(result.getEvent().getKey()).isNotEmpty();
+  }
+
+  @Test
+  public void create_version_event() {
+    SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto());
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setCategory(VERSION)
+      .setName("5.6.3")
+      .setDescription("LTS version");
+
+    call(request);
+
+    Optional<SnapshotDto> newAnalysis = dbClient.snapshotDao().selectByUuid(dbSession, analysis.getUuid());
+    assertThat(newAnalysis.get().getVersion()).isEqualTo("5.6.3");
+  }
+
+  @Test
+  public void create_other_event_with_ws_response() {
+    SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto());
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setName("Project Import")
+      .setDescription("Import from another instance");
+
+    CreateEventResponse result = call(request);
+
+    SnapshotDto newAnalysis = dbClient.snapshotDao().selectByUuid(dbSession, analysis.getUuid()).get();
+    assertThat(analysis.getVersion()).isEqualTo(newAnalysis.getVersion());
+    ProjectAnalyses.Event event = result.getEvent();
+    assertThat(event.getKey()).isNotEmpty();
+    assertThat(event.getCategory()).isEqualTo(OTHER.name());
+    assertThat(event.getName()).isEqualTo("Project Import");
+    assertThat(event.getDescription()).isEqualTo("Import from another instance");
+    assertThat(event.getAnalysis()).isEqualTo(analysis.getUuid());
+  }
+
+  @Test
+  public void create_event_without_description() {
+    SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto());
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setCategory(OTHER)
+      .setName("Project Import");
+
+    CreateEventResponse result = call(request);
+
+    ProjectAnalyses.Event event = result.getEvent();
+    assertThat(event.getKey()).isNotEmpty();
+    assertThat(event.hasDescription()).isFalse();
+  }
+
+  @Test
+  public void create_2_version_events_on_same_project() {
+    ComponentDto project = newProjectDto();
+    SnapshotDto firstAnalysis = db.components().insertProjectAndSnapshot(project);
+    CreateEventRequest.Builder firstRequest = CreateEventRequest.builder()
+      .setAnalysis(firstAnalysis.getUuid())
+      .setCategory(VERSION)
+      .setName("5.6.3");
+    SnapshotDto secondAnalysis = dbClient.snapshotDao().insert(dbSession, newAnalysis(project));
+    db.commit();
+    CreateEventRequest.Builder secondRequest = CreateEventRequest.builder()
+      .setAnalysis(secondAnalysis.getUuid())
+      .setCategory(VERSION)
+      .setName("6.3");
+
+    call(firstRequest);
+    call(secondRequest);
+
+    List<EventDto> events = dbClient.eventDao().selectByComponentUuid(dbSession, project.uuid());
+    assertThat(events).hasSize(2);
+  }
+
+  @Test
+  public void fail_if_analysis_is_not_found() {
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Analysis 'A42' is not found");
+
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis("A42")
+      .setCategory(OTHER)
+      .setName("Project Import");
+
+    call(request);
+  }
+
+  @Test
+  public void fail_if_2_version_events_on_the_same_analysis() {
+    SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto());
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setCategory(VERSION)
+      .setName("5.6.3");
+    call(request);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("A version event already exists on analysis '" + analysis.getUuid() + "'");
+
+    call(request.setName("6.3"));
+  }
+
+  @Test
+  public void fail_if_2_other_events_on_same_analysis_with_same_name() {
+    SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto());
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setCategory(OTHER)
+      .setName("Project Import");
+    call(request);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("An 'other' event with the same name already exists on analysis '" + analysis.getUuid() + "'");
+
+    call(request.setName("Project Import"));
+  }
+
+  @Test
+  public void fail_if_category_other_than_authorized() {
+    expectedException.expect(IllegalArgumentException.class);
+
+    SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto());
+    ws.newRequest()
+      .setParam(PARAM_ANALYSIS, analysis.getUuid())
+      .setParam(PARAM_NAME, "Project Import")
+      .setParam(PARAM_CATEGORY, "QP")
+      .execute();
+  }
+
+  @Test
+  public void fail_if_create_on_other_than_a_project() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("An event must be created on a project");
+
+    SnapshotDto analysis = db.components().insertViewAndSnapshot(newView());
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setCategory(VERSION)
+      .setName("5.6.3");
+
+    call(request);
+  }
+
+  @Test
+  public void fail_if_project_is_not_found() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Project of analysis 'A1' is not found");
+
+    SnapshotDto analysis = dbClient.snapshotDao().insert(dbSession, newSnapshot().setUuid("A1"));
+    db.commit();
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setCategory(VERSION)
+      .setName("5.6.3");
+
+    call(request);
+  }
+
+  @Test
+  public void fail_if_insufficient_permissions() {
+    SnapshotDto analysis = db.components().insertProjectAndSnapshot(newProjectDto("P1"));
+    CreateEventRequest.Builder request = CreateEventRequest.builder()
+      .setAnalysis(analysis.getUuid())
+      .setCategory(VERSION)
+      .setName("5.6.3")
+      .setDescription("LTS version");
+    userSession.anonymous();
+
+    expectedException.expect(ForbiddenException.class);
+
+    call(request);
+
+  }
+
+  @Test
+  public void ws_parameters() {
+    WebService.Action definition = ws.getDef();
+
+    assertThat(definition.isPost()).isTrue();
+    assertThat(definition.key()).isEqualTo("create_event");
+    assertThat(definition.responseExampleAsString()).isNotEmpty();
+  }
+
+  private CreateEventResponse call(CreateEventRequest.Builder requestBuilder) {
+    CreateEventRequest request = requestBuilder.build();
+    TestRequest httpRequest = ws.newRequest()
+      .setMethod(POST.name())
+      .setMediaType(MediaTypes.PROTOBUF);
+
+    httpRequest.setParam(PARAM_CATEGORY, request.getCategory().name())
+      .setParam(PARAM_NAME, request.getName())
+      .setParam(PARAM_ANALYSIS, request.getAnalysis());
+
+    if (request.getDescription() != null) {
+      httpRequest.setParam(PARAM_DESCRIPTION, request.getDescription());
+    }
+
+    try {
+      return CreateEventResponse.parseFrom(httpRequest.execute().getInputStream());
+    } catch (IOException e) {
+      throw Throwables.propagate(e);
+    }
+  }
+}
index c1a5eabda55fb09df077500d3a0246c1fe5d17c2..f9f6f716355fe9c7c4640549345b11b158fd856b 100644 (file)
@@ -138,6 +138,10 @@ public class SnapshotDao implements Dao {
     insert(session, Lists.asList(item, others));
   }
 
+  public void updateVersion(DbSession dbSession, String analysisUuid, String version) {
+    mapper(dbSession).updateVersion(analysisUuid, version);
+  }
+
   /**
    * Used by Governance
    */
index c5c7aef8f5cd5ad6a637ebc58840136b00dd2df8..52185ad878e9862c498403325fecfc5c6d36d419 100644 (file)
@@ -55,4 +55,6 @@ public interface SnapshotMapper {
   void unsetIsLastFlagForComponentUuid(@Param("componentUuid") String componentUuid);
 
   void setIsLastFlagForAnalysisUuid(@Param("analysisUuid") String analysisUuid);
+
+  void updateVersion(@Param("analysisUuid") String analysisUuid, @Param("version") String version);
 }
index b28b33e1647815d508a3f3f71cb133e601556e30..3c91a35d15ffd25e87b0793271192e080cab7e51 100644 (file)
@@ -29,12 +29,21 @@ public class EventDao implements Dao {
     return session.getMapper(EventMapper.class).selectByComponentUuid(componentUuid);
   }
 
-  public void insert(DbSession session, EventDto dto) {
+  public List<EventDto> selectByAnalysisUuid(DbSession dbSession, String uuid) {
+    return mapper(dbSession).selectByAnalysisUuid(uuid);
+  }
+
+  public EventDto insert(DbSession session, EventDto dto) {
     session.getMapper(EventMapper.class).insert(dto);
+
+    return dto;
   }
 
   public void delete(DbSession session, Long id) {
     session.getMapper(EventMapper.class).delete(id);
   }
 
+  private static EventMapper mapper(DbSession session) {
+    return session.getMapper(EventMapper.class);
+  }
 }
index 18e2845f5afbd97ac891ce0b271af4c402813745..58aba3b8fac15a046dc67e71f374f3a5d140c17c 100644 (file)
@@ -22,6 +22,10 @@ package org.sonar.db.event;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
+import static org.sonar.db.event.EventValidator.checkEventCategory;
+import static org.sonar.db.event.EventValidator.checkEventDescription;
+import static org.sonar.db.event.EventValidator.checkEventName;
+
 public class EventDto {
 
   public static final String CATEGORY_VERSION = "Version";
@@ -80,7 +84,7 @@ public class EventDto {
   }
 
   public EventDto setName(String name) {
-    this.name = name;
+    this.name = checkEventName(name);
     return this;
   }
 
@@ -89,7 +93,7 @@ public class EventDto {
   }
 
   public EventDto setCategory(String category) {
-    this.category = category;
+    this.category = checkEventCategory(category);
     return this;
   }
 
@@ -127,7 +131,7 @@ public class EventDto {
   }
 
   public EventDto setDescription(@Nullable String description) {
-    this.description = description;
+    this.description = checkEventDescription(description);
     return this;
   }
 
index d7a9190abbcdf9f1bd6e702b2051838ccdacfa1d..c8eb24e680e453e7f0001ddcf6eb83d987571faf 100644 (file)
@@ -23,10 +23,11 @@ import java.util.List;
 
 public interface EventMapper {
 
-  List<EventDto> selectByComponentUuid(String uuid);
+  List<EventDto> selectByComponentUuid(String componentUuid);
+
+  List<EventDto> selectByAnalysisUuid(String analysisUuid);
 
   void insert(EventDto dto);
 
   void delete(long id);
-
 }
diff --git a/sonar-db/src/main/java/org/sonar/db/event/EventValidator.java b/sonar-db/src/main/java/org/sonar/db/event/EventValidator.java
new file mode 100644 (file)
index 0000000..61d7f09
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.db.event;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+public class EventValidator {
+  private static final int MAX_NAME_LENGTH = 400;
+  private static final int MAX_CATEGORY_LENGTH = 50;
+  private static final int MAX_DESCRIPTION_LENGTH = 4000;
+
+  private EventValidator() {
+    // prevent instantiation
+  }
+
+  public static String checkEventName(String name) {
+    checkArgument(name.length() <= MAX_NAME_LENGTH, "Event name length (%s) is longer than the maximum authorized (%s). '%s' was provided.",
+      name.length(), MAX_NAME_LENGTH, name);
+    return name;
+  }
+
+  public static String checkEventCategory(String category) {
+    checkArgument(category.length() <= MAX_CATEGORY_LENGTH, "Event category length (%s) is longer than the maximum authorized (%s). '%s' was provided.",
+      category.length(), MAX_CATEGORY_LENGTH, category);
+    return category;
+  }
+
+  @CheckForNull
+  public static String checkEventDescription(@Nullable String description) {
+    if (description == null) {
+      return null;
+    }
+
+    checkArgument(description.length() <= MAX_DESCRIPTION_LENGTH, "Event description length (%s) is longer than the maximum authorized (%s). '%s' was provided.",
+      description.length(), MAX_DESCRIPTION_LENGTH, description);
+    return description;
+  }
+
+}
index 98166bd23c14e44350e39b7ed176bd6ca23e25ad..b27c1e31abffe71b26f748bd4d33a981b8e58afc 100644 (file)
     where uuid = #{analysisUuid}
   </update>
 
+  <update id="updateVersion" parameterType="map">
+    update snapshots
+    set version = #{version, jdbcType=VARCHAR}
+    where uuid = #{analysisUuid}
+  </update>
+
   <insert id="insert" parameterType="Snapshot" keyColumn="id" useGeneratedKeys="true" keyProperty="id">
     insert into snapshots (
     uuid,
     #{period4Date, jdbcType=BIGINT},
     #{period5Date, jdbcType=BIGINT})
   </insert>
-
 </mapper>
 
index ad24d2d78aa96efa32c5117b42e996c76dba7853..4f4fd4d6a2e8b8680971e636c39416019cfd3772 100644 (file)
     </where>
   </select>
 
+  <select id="selectByAnalysisUuid" parameterType="String" resultType="Event">
+    SELECT
+    <include refid="eventColumns"/>
+    FROM events e
+    <where>
+      AND e.analysis_uuid=#{analysisUuid}
+    </where>
+  </select>
+
   <insert id="insert" parameterType="Event" keyColumn="id" useGeneratedKeys="true" keyProperty="id">
     INSERT INTO events (uuid, analysis_uuid, component_uuid, name, category, description, event_data, event_date, created_at)
     VALUES (
index e3fde30440333cba93dcbd19a5d7da678606d990..3b32b88b9a4ce6f6a5cb2efc7a770554faf8f330 100644 (file)
@@ -65,6 +65,7 @@ import org.sonar.api.utils.System2;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.core.util.SequenceUuidFactory;
 import org.sonar.db.component.ComponentDbTester;
+import org.sonar.db.event.EventDbTester;
 import org.sonar.db.organization.OrganizationDbTester;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.organization.OrganizationTesting;
@@ -99,6 +100,7 @@ public class DbTester extends ExternalResource {
 
   private final UserDbTester userTester;
   private final ComponentDbTester componentTester;
+  private final EventDbTester eventTester;
   private final OrganizationDbTester organizationTester;
   private final PermissionTemplateDbTester permissionTemplateTester;
   private final QualityGateDbTester qualityGateDbTester;
@@ -110,6 +112,7 @@ public class DbTester extends ExternalResource {
     initDbClient();
     this.userTester = new UserDbTester(this);
     this.componentTester = new ComponentDbTester(this);
+    this.eventTester = new EventDbTester(this);
     this.organizationTester = new OrganizationDbTester(this);
     this.permissionTemplateTester = new PermissionTemplateDbTester(this);
     this.qualityGateDbTester = new QualityGateDbTester(this);
@@ -177,6 +180,10 @@ public class DbTester extends ExternalResource {
     return componentTester;
   }
 
+  public EventDbTester events() {
+    return eventTester;
+  }
+
   public OrganizationDbTester organizations() {
     return organizationTester;
   }
index be33663ff031245a03b0bb725d0214b34a41a9ee..dac0d64a2d577b03f786ef90f6cf52c8d337239b 100644 (file)
@@ -35,6 +35,7 @@ import static com.google.common.collect.Lists.newArrayList;
 import static java.util.Collections.emptyList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.sonar.db.component.ComponentTesting.newProjectDto;
+import static org.sonar.db.component.SnapshotDto.STATUS_PROCESSED;
 import static org.sonar.db.component.SnapshotQuery.SORT_FIELD.BY_DATE;
 import static org.sonar.db.component.SnapshotQuery.SORT_ORDER.ASC;
 import static org.sonar.db.component.SnapshotQuery.SORT_ORDER.DESC;
@@ -296,6 +297,18 @@ public class SnapshotDaoTest {
     verifyStatusAndIsLastFlag("A3", SnapshotDto.STATUS_PROCESSED, true);
   }
 
+  @Test
+  public void updateVersion() {
+    insertAnalysis("P1", "A1", STATUS_PROCESSED, true);
+    db.commit();
+
+    underTest.updateVersion(dbSession, "A1", "5.6.3");
+
+    SnapshotDto result = underTest.selectByUuid(dbSession, "A1").get();
+
+    assertThat(result.getVersion()).isEqualTo("5.6.3");
+  }
+
   private void insertAnalysis(String projectUuid, String uuid, String status, boolean isLastFlag) {
     SnapshotDto snapshot = SnapshotTesting.newAnalysis(ComponentTesting.newProjectDto(projectUuid))
       .setLast(isLastFlag)
index c61b79b2120f7bc0eda186315a1a2b44bccd141b..1bd351dbf6c2be8f3888fd8f7a3ba7582f31efc4 100644 (file)
@@ -24,9 +24,16 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.component.ComponentTesting.newProjectDto;
+import static org.sonar.db.component.SnapshotTesting.newAnalysis;
+import static org.sonar.db.event.EventTesting.newEvent;
 
 public class EventDaoTest {
 
@@ -34,6 +41,8 @@ public class EventDaoTest {
   public ExpectedException expectedException = ExpectedException.none();
   @Rule
   public DbTester dbTester = DbTester.create(System2.INSTANCE);
+  private DbClient dbClient = dbTester.getDbClient();
+  private DbSession dbSession = dbTester.getSession();
 
   EventDao underTest = dbTester.getDbClient().eventDao();
 
@@ -60,6 +69,25 @@ public class EventDaoTest {
     assertThat(dto.getCreatedAt()).isEqualTo(1225630680000L);
   }
 
+  @Test
+  public void select_by_analysis_uuid() {
+    ComponentDto project = newProjectDto();
+    SnapshotDto analysis = dbTester.components().insertProjectAndSnapshot(project);
+    SnapshotDto otherAnalysis = dbClient.snapshotDao().insert(dbSession, newAnalysis(project));
+    dbTester.commit();
+    dbTester.events().insertEvent(newEvent(analysis).setUuid("A1"));
+    dbTester.events().insertEvent(newEvent(otherAnalysis).setUuid("O1"));
+    dbTester.events().insertEvent(newEvent(analysis).setUuid("A2"));
+    dbTester.events().insertEvent(newEvent(otherAnalysis).setUuid("O2"));
+    dbTester.events().insertEvent(newEvent(analysis).setUuid("A3"));
+    dbTester.events().insertEvent(newEvent(otherAnalysis).setUuid("O3"));
+
+    List<EventDto> result = underTest.selectByAnalysisUuid(dbSession, analysis.getUuid());
+
+    assertThat(result).hasSize(3);
+    assertThat(result).extracting(EventDto::getUuid).containsOnly("A1", "A2", "A3");
+  }
+
   @Test
   public void return_different_categories() {
     dbTester.prepareDbUnit(getClass(), "shared.xml");
diff --git a/sonar-db/src/test/java/org/sonar/db/event/EventDbTester.java b/sonar-db/src/test/java/org/sonar/db/event/EventDbTester.java
new file mode 100644 (file)
index 0000000..dc55f5c
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.db.event;
+
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+
+public class EventDbTester {
+  private final DbTester db;
+  private final DbClient dbClient;
+  private final DbSession dbSession;
+
+  public EventDbTester(DbTester db) {
+    this.db = db;
+    this.dbClient = db.getDbClient();
+    this.dbSession = db.getSession();
+  }
+
+  public EventDto insertEvent(EventDto event) {
+    dbClient.eventDao().insert(dbSession, event);
+    db.commit();
+
+    return event;
+  }
+
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/event/EventTesting.java b/sonar-db/src/test/java/org/sonar/db/event/EventTesting.java
new file mode 100644 (file)
index 0000000..5df8844
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.db.event;
+
+import org.sonar.db.component.SnapshotDto;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+
+public class EventTesting {
+
+  public static EventDto newEvent(SnapshotDto analysis) {
+    requireNonNull(analysis.getUuid());
+    requireNonNull(analysis.getComponentUuid());
+
+    return new EventDto()
+      .setAnalysisUuid(analysis.getUuid())
+      .setComponentUuid(analysis.getComponentUuid())
+      .setUuid(randomAlphanumeric(40))
+      .setName(randomAlphanumeric(400))
+      .setDescription(randomAlphanumeric(400))
+      .setCategory(randomAlphanumeric(50))
+      .setComponentUuid(analysis.getComponentUuid())
+      .setCreatedAt(System.currentTimeMillis())
+      .setDate(System.currentTimeMillis());
+  }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/event/EventValidatorTest.java b/sonar-db/src/test/java/org/sonar/db/event/EventValidatorTest.java
new file mode 100644 (file)
index 0000000..f40b459
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.db.event;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static com.google.common.base.Strings.repeat;
+
+public class EventValidatorTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void valid_cases() {
+    EventValidator.checkEventName(repeat("a", 400));
+    EventValidator.checkEventCategory(repeat("a", 50));
+    EventValidator.checkEventDescription(repeat("a", 4000));
+    EventValidator.checkEventDescription(null);
+  }
+
+  @Test
+  public void fail_if_name_longer_than_400() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Event name length (401) is longer than the maximum authorized (400).");
+
+    EventValidator.checkEventName(repeat("a", 400 + 1));
+  }
+
+  @Test
+  public void fail_if_category_longer_than_50() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Event category length (51) is longer than the maximum authorized (50). 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' was provided.");
+
+    EventValidator.checkEventCategory(repeat("a", 50 + 1));
+  }
+
+  @Test
+  public void fail_if_description_longer_than_4000() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Event description length (4001) is longer than the maximum authorized (4000).");
+
+    EventValidator.checkEventDescription(repeat("a", 4000 + 1));
+  }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/CreateEventRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/CreateEventRequest.java
new file mode 100644 (file)
index 0000000..a47238e
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonarqube.ws.client.projectanalysis;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+public class CreateEventRequest {
+  private final String analysis;
+  private final Category category;
+  private final String name;
+  private final String description;
+
+  private CreateEventRequest(Builder builder) {
+    analysis = builder.analysis;
+    category = builder.category;
+    name = builder.name;
+    description = builder.description;
+  }
+
+  public String getAnalysis() {
+    return analysis;
+  }
+
+  public Category getCategory() {
+    return category;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  @CheckForNull
+  public String getDescription() {
+    return description;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private String analysis;
+    private Category category = Category.OTHER;
+    private String name;
+    private String description;
+
+    private Builder() {
+      // enforce static factory method
+    }
+
+    public Builder setAnalysis(String analysis) {
+      this.analysis = analysis;
+      return this;
+    }
+
+    public Builder setCategory(Category category) {
+      this.category = category;
+      return this;
+    }
+
+    public Builder setName(String name) {
+      this.name = name;
+      return this;
+    }
+
+    public Builder setDescription(@Nullable String description) {
+      this.description = description;
+      return this;
+    }
+
+    public CreateEventRequest build() {
+      checkArgument(analysis != null, "Analysis key is required");
+      checkArgument(category != null, "Category is required");
+      checkArgument(name != null, "Name is required");
+
+      return new CreateEventRequest(this);
+    }
+  }
+
+  public enum Category {
+    VERSION("Version"), OTHER("Other");
+
+    private final String label;
+
+    Category(String label) {
+      this.label = label;
+    }
+
+    public String getLabel() {
+      return label;
+    }
+
+    public static Category fromLabel(String label) {
+      for (Category category : values()) {
+        if (category.getLabel().equals(label)) {
+          return category;
+        }
+      }
+
+      throw new IllegalArgumentException("Unknown event category label '" + label + "'");
+    }
+  }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/ProjectAnalysesWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/ProjectAnalysesWsParameters.java
new file mode 100644 (file)
index 0000000..0b1cfd8
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonarqube.ws.client.projectanalysis;
+
+public class ProjectAnalysesWsParameters {
+  public static final String PARAM_ANALYSIS = "analysis";
+  public static final String PARAM_CATEGORY = "category";
+  public static final String PARAM_NAME = "name";
+  public static final String PARAM_DESCRIPTION = "description";
+
+  private ProjectAnalysesWsParameters() {
+    // static access only
+  }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/package-info.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalysis/package-info.java
new file mode 100644 (file)
index 0000000..cd3561a
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonarqube.ws.client.projectanalysis;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-ws/src/main/protobuf/ws-projectanalyses.proto b/sonar-ws/src/main/protobuf/ws-projectanalyses.proto
new file mode 100644 (file)
index 0000000..686027a
--- /dev/null
@@ -0,0 +1,38 @@
+// SonarQube, open source software quality management tool.
+// Copyright (C) 2008-2016 SonarSource
+// mailto:contact AT sonarsource DOT com
+//
+// SonarQube is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 3 of the License, or (at your option) any later version.
+//
+// SonarQube is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+syntax = "proto2";
+
+package sonarqube.ws.projectanalysis;
+
+option java_package = "org.sonarqube.ws";
+option java_outer_classname = "ProjectAnalyses";
+option optimize_for = SPEED;
+
+// WS api/project_analyses/create_event
+message CreateEventResponse {
+  optional Event event = 1;
+}
+
+message Event {
+  optional string key = 1;
+  optional string analysis = 2;
+  optional string category = 3;
+  optional string name = 4;
+  optional string description = 5;
+}
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/CreateEventRequestTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/CreateEventRequestTest.java
new file mode 100644 (file)
index 0000000..a96e1e0
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonarqube.ws.client.projectanalysis;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonarqube.ws.client.projectanalysis.CreateEventRequest.Category.OTHER;
+import static org.sonarqube.ws.client.projectanalysis.CreateEventRequest.Category.VERSION;
+
+public class CreateEventRequestTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private CreateEventRequest.Builder underTest = CreateEventRequest.builder();
+
+  @Test
+  public void build_request() {
+    CreateEventRequest result = underTest.setAnalysis("P1").setCategory(OTHER).setName("name").setDescription("description").build();
+
+    assertThat(result.getAnalysis()).isEqualTo("P1");
+    assertThat(result.getCategory()).isEqualTo(OTHER);
+    assertThat(result.getName()).isEqualTo("name");
+    assertThat(result.getDescription()).isEqualTo("description");
+  }
+
+  @Test
+  public void other_category_by_default() {
+    CreateEventRequest result = underTest.setAnalysis("P1").setName("name").build();
+
+    assertThat(OTHER.equals(result.getCategory()));
+  }
+
+  @Test
+  public void fail_when_no_category() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Category is required");
+
+    underTest.setAnalysis("P1").setName("name").setCategory(null).build();
+  }
+
+  @Test
+  public void fail_when_no_analysis() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Analysis key is required");
+
+    underTest.setCategory(VERSION).setName("name").build();
+  }
+
+  @Test
+  public void fail_when_no_name() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Name is required");
+
+    underTest.setAnalysis("P1").setCategory(VERSION).build();
+  }
+}
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/ProjectAnalysesWsParametersTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/projectanalysis/ProjectAnalysesWsParametersTest.java
new file mode 100644 (file)
index 0000000..f431cbd
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonarqube.ws.client.projectanalysis;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.test.TestUtils.hasOnlyPrivateConstructors;
+
+public class ProjectAnalysesWsParametersTest {
+
+  @Test
+  public void private_access_only() {
+    assertThat(hasOnlyPrivateConstructors(ProjectAnalysesWsParameters.class)).isTrue();
+  }
+}