<modules>
<module>sonar-application</module>
<module>sonar-process</module>
+ <module>sonar-search</module>
<module>sonar-batch</module>
<module>sonar-batch-maven-compat</module>
<module>sonar-check-api</module>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar</artifactId>
+ <version>4.5-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>sonar-search</artifactId>
+ <packaging>jar</packaging>
+ <name>SonarQube :: Search</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-process</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.elasticsearch</groupId>
+ <artifactId>elasticsearch</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.easytesting</groupId>
+ <artifactId>fest-assert</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
\ No newline at end of file
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.
+ */
+package org.sonar.search.script;
+
+import com.google.common.collect.ImmutableSet;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.xcontent.support.XContentMapValues;
+import org.elasticsearch.script.AbstractExecutableScript;
+import org.elasticsearch.script.ExecutableScript;
+import org.elasticsearch.script.NativeScriptFactory;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * @since 4.4
+ */
+public class ListUpdate extends AbstractExecutableScript {
+
+ public static final String NAME = "listUpdate";
+
+ public static final String ID_FIELD = "idField";
+ public static final String ID_VALUE = "idValue";
+ public static final String FIELD = "field";
+ public static final String VALUE = "value";
+
+ public static class UpdateListScriptFactory implements NativeScriptFactory {
+ @Override
+ public ExecutableScript newScript(@Nullable Map<String, Object> params) {
+ String idField = XContentMapValues.nodeStringValue(params.get(ID_FIELD), null);
+ String idValue = XContentMapValues.nodeStringValue(params.get(ID_VALUE), null);
+ String field = XContentMapValues.nodeStringValue(params.get(FIELD), null);
+ Map value = null;
+ if (idField == null) {
+ throw new IllegalStateException("Missing '" + ID_FIELD + "' parameter");
+ }
+ if (idValue == null) {
+ throw new IllegalStateException("Missing '" + ID_VALUE + "' parameter");
+ }
+ if (field == null) {
+ throw new IllegalStateException("Missing '" + FIELD + "' parameter");
+ }
+
+ //NULL case is deletion of nested item
+ if (params.containsKey(VALUE)) {
+ Object obj = params.get(VALUE);
+ if (obj != null) {
+ value = XContentMapValues.nodeMapValue(params.get(VALUE), "Update item");
+ }
+ }
+
+ return new ListUpdate(idField, idValue, field, value);
+ }
+ }
+
+
+ private final String idField;
+ private final String idValue;
+ private final String field;
+ private final Map<String, Object> value;
+
+ private Map<String, Object> ctx;
+
+ public ListUpdate(String idField, String idValue, String field, Map<String, Object> value) {
+ this.idField = idField;
+ this.idValue = idValue;
+ this.field = field;
+ this.value = value;
+ }
+
+ @Override
+ public void setNextVar(String name, Object value) {
+ if (name.equals("ctx")) {
+ ctx = (Map<String, Object>) value;
+ }
+ }
+
+ @Override
+ public Object unwrap(Object value) {
+ return value;
+ }
+
+ @Override
+ public Object run() {
+ try {
+ //Get the Document's source from ctx
+ Map<String, Object> source = XContentMapValues.nodeMapValue(ctx.get("_source"), "source from context");
+
+ //Get the Object for list update
+ Object fieldValue = source.get(field);
+
+ if (fieldValue == null && value != null) {
+ // 0. The field does not exist (this is a upsert then)
+ source.put(field, value);
+ } else if (!XContentMapValues.isArray(fieldValue) && value != null) {
+ // 1. The field is not yet a list
+ Map currentFieldValue = XContentMapValues.nodeMapValue(fieldValue, "current FieldValue");
+ if (XContentMapValues.nodeStringValue(currentFieldValue.get(idField), null).equals(idValue)) {
+ source.put(field, value);
+ } else {
+ source.put(field, ImmutableSet.of(fieldValue, value));
+ }
+ } else {
+ // 3. field is a list
+ Collection items = (Collection) fieldValue;
+ Object target = null;
+ for (Object item : items) {
+ Map<String, Object> fields = (Map<String, Object>) item;
+ String itemIdValue = XContentMapValues.nodeStringValue(fields.get(idField), null);
+ if (itemIdValue != null && itemIdValue.equals(idValue)) {
+ target = item;
+ break;
+ }
+ }
+ if (target != null) {
+ items.remove(target);
+ }
+
+ //Supporting the update by NULL = deletion case
+ if (value != null) {
+ items.add(value);
+ }
+ source.put(field, items);
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("failed to execute listUpdate script", e);
+ }
+ return null;
+
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!--
+ Configuration for default logger. Only used while embedded server is starting,
+ before proper logging configuration is loaded.
+
+ See http://logback.qos.ch/manual/configuration.html
+-->
+<configuration debug="false">
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+
+ <appender name="LOGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+ <File>${SONAR_HOME}/logs/sonar.log</File>
+ <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
+ <param name="FileNamePattern" value="${SONAR_HOME}/logs/sonar.%i.log"/>
+ <param name="MinIndex" value="1"/>
+ <param name="MaxIndex" value="3"/>
+ </rollingPolicy>
+ <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
+ <param name="MaxFileSize" value="5MB"/>
+ </triggeringPolicy>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <!-- Use %d{yyyy.MM.dd HH:mm:ss.SSS} to display milliseconds -->
+ <pattern>
+ %d{yyyy.MM.dd HH:mm:ss} %-5level [%logger{20}] %X %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>
+ %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <logger name="console" additivity="false">
+ <level value="INFO"/>
+ <appender-ref ref="CONSOLE"/>
+ </logger>
+
+ <root>
+ <level value="INFO"/>
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+</configuration>
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.
+ */
+package org.sonar.search;
+
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.client.transport.TransportClient;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.transport.InetSocketTransportAddress;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.SocketException;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class ElasticSearchTest {
+
+ DatagramSocket socket;
+ ElasticSearch elasticSearch;
+
+ @Before
+ public void setup() throws SocketException {
+ socket = new DatagramSocket(0);
+ elasticSearch = new ElasticSearch("test", socket.getLocalPort());
+ }
+
+ @After
+ public void tearDown() {
+ socket.close();
+ elasticSearch.shutdown();
+ }
+
+ @Test
+ public void can_connect() throws IOException, InterruptedException {
+
+ // 0 assert that application is running
+ assertHeartBeat();
+
+ Settings settings = ImmutableSettings.settingsBuilder()
+ .put("client.transport.sniff", true)
+ .put("cluster.name", "sonarqube")
+ .build();
+ TransportClient client = new TransportClient(settings)
+ .addTransportAddress(new InetSocketTransportAddress("localhost", 9300));
+
+
+ // 0 assert that we have a OK cluster available
+ assertThat(client.admin().cluster().prepareClusterStats().get().getStatus()).isEqualTo(ClusterHealthStatus.GREEN);
+
+ // 1 assert that we get a heartBeat from the application
+ assertHeartBeat();
+ }
+
+ private void assertHeartBeat() throws IOException {
+ DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
+ socket.receive(packet);
+ assertThat(packet.getData()).isNotNull();
+ }
+}
\ No newline at end of file