import java.util.List;
@Properties({
+ @Property(
+ key = CoreProperties.ORGANIZATION,
+ name = "Organization",
+ description = "Identify your installation. Required to generate the server key and to benefit from licensed plugins. Server must be restarted for the change to take effect.",
+ global = true),
@Property(
key = CoreProperties.CORE_COVERAGE_PLUGIN_PROPERTY,
defaultValue = "cobertura",
public class ServerMetadata extends Server {
- private String id;
- private String version;
- private String url;
- private Date startTime;
+ private Configuration conf;
public ServerMetadata(Configuration conf) {
- id = conf.getString(CoreProperties.SERVER_ID);
- version = conf.getString(CoreProperties.SERVER_VERSION);
- url = getURL(conf);
- String dateString = conf.getString(CoreProperties.SERVER_STARTTIME);
- if (dateString!=null) {
- try {
- this.startTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateString);
-
- } catch (ParseException e) {
- LoggerFactory.getLogger(getClass()).error("The property " + CoreProperties.SERVER_STARTTIME + " is badly formatted.", e);
- }
- }
- }
-
- public static String getURL(Configuration conf) {
- return StringUtils.removeEnd(conf.getString("sonar.host.url", "http://localhost:9000"), "/");
+ this.conf = conf;
}
public String getId() {
- return id;
+ return conf.getString(CoreProperties.SERVER_ID);
}
public String getVersion() {
- return version;
+ return conf.getString(CoreProperties.SERVER_VERSION);
}
public Date getStartedAt() {
- return startTime;
+ String dateString = conf.getString(CoreProperties.SERVER_STARTTIME);
+ if (dateString != null) {
+ try {
+ return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateString);
+
+ } catch (ParseException e) {
+ LoggerFactory.getLogger(getClass()).error("The property " + CoreProperties.SERVER_STARTTIME + " is badly formatted.", e);
+ }
+ }
+ return null;
}
public String getURL() {
- return url;
+ return StringUtils.removeEnd(conf.getString("sonar.host.url", "http://localhost:9000"), "/");
+ }
+
+ @Override
+ public String getKey() {
+ return conf.getString(CoreProperties.SERVER_KEY);
}
}
String TIMEMACHINE_DEFAULT_PERIOD_3 = "30";
String TIMEMACHINE_DEFAULT_PERIOD_4 = "";
String TIMEMACHINE_DEFAULT_PERIOD_5 = "";
+
+ /**
+ * @since 2.11
+ */
+ String ORGANIZATION = "sonar.organization";
+
+ /**
+ * @since 2.11
+ */
+ String SERVER_KEY = "sonar.serverKey.secured";
}
* @since 2.4
*/
public abstract String getURL();
+
+ /**
+ * @since 2.10
+ */
+ public abstract String getKey();
}
coreContainer = rootContainer.makeChildContainer();
coreContainer.as(Characteristics.CACHE).addComponent(PluginDeployer.class);
coreContainer.as(Characteristics.CACHE).addComponent(ServerPluginRepository.class);
- coreContainer.as(Characteristics.CACHE).addComponent(ServerImpl.class);
coreContainer.as(Characteristics.CACHE).addComponent(DefaultServerFileSystem.class);
coreContainer.as(Characteristics.CACHE).addComponent(ThreadLocalDatabaseSessionFactory.class);
coreContainer.as(Characteristics.CACHE).addComponent(HttpDownloader.class);
ServerPluginRepository pluginRepository = servicesContainer.getComponent(ServerPluginRepository.class);
pluginRepository.registerExtensions(servicesContainer);
+ servicesContainer.as(Characteristics.CACHE).addComponent(ServerImpl.class);
servicesContainer.as(Characteristics.CACHE).addComponent(DefaultModelFinder.class); // depends on plugins
servicesContainer.as(Characteristics.CACHE).addComponent(DefaultModelManager.class);
servicesContainer.as(Characteristics.CACHE).addComponent(Plugins.class);
*/
package org.sonar.server.platform;
+import org.apache.commons.configuration.Configuration;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
import org.sonar.api.platform.Server;
import java.io.IOException;
public final class ServerImpl extends Server {
- private final String id;
- private final String version;
+ private String id;
+ private String version;
private final Date startedAt;
+ private String key;
+ private Configuration conf;
- public ServerImpl() {
+ public ServerImpl(Configuration conf) {
+ this(conf, new Date());
+ }
+
+ ServerImpl(Configuration conf, Date startedAt) {
+ this.conf = conf;
+ this.startedAt = startedAt;
+ }
+
+ public void start() {
try {
- this.startedAt = new Date();
- this.id = new SimpleDateFormat("yyyyMMddHHmmss").format(startedAt);
- this.version = loadVersionFromManifest("/META-INF/maven/org.codehaus.sonar/sonar-plugin-api/pom.properties");
- if (StringUtils.isBlank(this.version)) {
+ id = new SimpleDateFormat("yyyyMMddHHmmss").format(startedAt);
+ key = initKey(conf);
+ version = loadVersionFromManifest("/META-INF/maven/org.codehaus.sonar/sonar-plugin-api/pom.properties");
+ if (StringUtils.isBlank(version)) {
throw new ServerStartException("Unknown Sonar version");
}
} catch (IOException e) {
- throw new ServerStartException("Can not load Sonar metadata", e);
+ throw new ServerStartException("Can not load metadata", e);
}
}
- public ServerImpl(String id, String version, Date startedAt) {
- this.id = id;
- this.version = version;
- this.startedAt = startedAt;
+ private String initKey(Configuration conf) {
+ String organization = conf.getString(CoreProperties.ORGANIZATION);
+ String previousKey = conf.getString(CoreProperties.SERVER_KEY);
+ return new ServerKeyGenerator().generate(organization, previousKey);
}
public String getId() {
return null;
}
+ public String getKey() {
+ return key;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.server.platform;
+
+import com.google.common.collect.Lists;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.Logs;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+import java.util.List;
+
+public class ServerKeyGenerator {
+
+ /**
+ * Increment this version each time the algorithm is changed. Do not exceed 9.
+ */
+ static final String VERSION = "1";
+
+ private static final int CHECKSUM_SIZE = 9;
+
+ public String generate(String organization) {
+ return generate(organization, null);
+ }
+
+ public String generate(String organization, String previousKey) {
+ List<ServerKey> serverKeys = generateForOrganization(organization);
+ String external = null;
+ String best = null;
+ for (ServerKey serverKey : serverKeys) {
+ if (StringUtils.equals(previousKey, serverKey.getKey())) {
+ best = serverKey.getKey();
+ }
+ if (serverKey.isExternal()) {
+ // External addresses are prefered to internal addresses.
+ external = serverKey.getKey();
+ }
+ }
+ if (best == null) {
+ if (external!=null) {
+ best = external;
+ } else if (!serverKeys.isEmpty()) {
+ best = serverKeys.get(0).getKey();
+ }
+ }
+ log(previousKey, best);
+ return best;
+ }
+
+ private void log(String previousKey, String newKey) {
+ if (StringUtils.isNotBlank(newKey) && StringUtils.isNotBlank(previousKey) && !previousKey.equals(newKey)) {
+ LoggerFactory.getLogger(getClass()).warn("Server key has changed. Licensed plugins may be disabled. "
+ + "Please check the organization name (Settings page) and the server IP addresses.");
+ }
+ if (StringUtils.isNotBlank(newKey)) {
+ Logs.INFO.info("Server key: " + newKey);
+
+ } else if (StringUtils.isNotBlank(previousKey)) {
+ LoggerFactory.getLogger(getClass()).warn("Server key has been removed. Licensed plugins may be disabled. "
+ + "Please check the organization name (Settings page) and the server IP addresses.");
+ }
+ }
+
+ List<ServerKey> generateForOrganization(String organization) {
+ List<ServerKey> keys = Lists.newArrayList();
+ if (StringUtils.isNotBlank(organization)) {
+ try {
+ Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
+ while (networkInterfaces.hasMoreElements()) {
+ NetworkInterface networkInterface = networkInterfaces.nextElement();
+ Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ ServerKey key = new ServerKey(organization, addresses.nextElement());
+ if (key.isValid()) {
+ keys.add(key);
+ }
+ }
+ }
+ } catch (SocketException e) {
+ LoggerFactory.getLogger(getClass()).error("Fail to generate server key. Network interfaces can't be browsed.", e);
+ }
+ }
+ return keys;
+ }
+
+ static class ServerKey {
+ private String organization;
+ private InetAddress address;
+
+ ServerKey(String organization, InetAddress address) {
+ this.organization = organization;
+ this.address = address;
+ }
+
+ boolean isExternal() {
+ return !address.isLoopbackAddress() && !address.isSiteLocalAddress() && !address.isLinkLocalAddress();
+ }
+
+ boolean isValid() {
+ // Loopback addresses are in the range 127/8.
+ // Link local addresses are in the range 169.254/16 (IPv4) or fe80::/10 (IPv6). They are "autoconfiguration" addresses.
+ // They can assigned pseudorandomly, so they don't guarantee to be the same between two server startups.
+ return !address.isLoopbackAddress() && !address.isLinkLocalAddress();
+ }
+
+ String getKey() {
+ String key = new StringBuilder().append(organization).append("-").append(address.getHostAddress()).toString();
+ return VERSION + DigestUtils.shaHex(key.getBytes()).substring(0, CHECKSUM_SIZE);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ServerKey serverKey = (ServerKey) o;
+ if (!address.equals(serverKey.address)) {
+ return false;
+ }
+ if (!organization.equals(serverKey.organization)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = organization.hashCode();
+ result = 31 * result + address.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return getKey();
+ }
+ }
+}
public void start() {
setProperty(CoreProperties.SERVER_ID, server.getId());
setProperty(CoreProperties.SERVER_VERSION, server.getVersion());
- if (server.getStartedAt() != null) {
- setProperty(CoreProperties.SERVER_STARTTIME, new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(server.getStartedAt()));
- }
+ setProperty(CoreProperties.SERVER_KEY, server.getKey());
+ setProperty(CoreProperties.SERVER_STARTTIME, new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(server.getStartedAt()));
session.commit();
}
private void setProperty(String key, String value) {
Property prop = session.getSingleResult(Property.class, "key", key);
- if (prop == null) {
- prop = new Property(key, value);
- } else {
- prop.setValue(value);
+
+ if (value == null && prop != null) {
+ session.removeWithoutFlush(prop);
+
+ } else if (value != null) {
+ if (prop == null) {
+ prop = new Property(key, value);
+ } else {
+ prop.setValue(value);
+ }
+ session.saveWithoutFlush(prop);
}
- session.save(prop);
}
-}
+}
\ No newline at end of file
def sonar_info
sonar_info=[]
+ add_property(sonar_info, 'Server Key') {org.sonar.server.platform.Platform.getServer().getKey()}
add_property(sonar_info, 'Version') {org.sonar.server.platform.Platform.getServer().getVersion()}
add_property(sonar_info, 'ID') {org.sonar.server.platform.Platform.getServer().getId()}
add_property(sonar_info, 'Database') {"#{jdbc_metadata. getDatabaseProductName()} #{jdbc_metadata. getDatabaseProductVersion()}"}
*/
package org.sonar.server.platform;
+import org.apache.commons.configuration.PropertiesConfiguration;
import org.junit.Test;
import java.io.IOException;
@Test
public void alwaysReturnTheSameValues() {
- ServerImpl server = new ServerImpl();
+ ServerImpl server = new ServerImpl(new PropertiesConfiguration());
+ server.start();
assertNotNull(server.getId());
assertEquals(server.getId(), server.getId());
@Test
public void getVersionFromFile() throws IOException {
- assertEquals("1.0", new ServerImpl().loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-with-version.properties"));
+ assertEquals("1.0", new ServerImpl(new PropertiesConfiguration()).loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-with-version.properties"));
}
@Test
public void testFileWithNoVersion() throws IOException {
- assertEquals("", new ServerImpl().loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-without-version.properties"));
+ assertEquals("", new ServerImpl(new PropertiesConfiguration()).loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-without-version.properties"));
}
@Test
public void testFileWithEmptyVersionParameter() throws IOException {
- assertEquals("", new ServerImpl().loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-with-empty-version.properties"));
+ assertEquals("", new ServerImpl(new PropertiesConfiguration()).loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-with-empty-version.properties"));
}
@Test
public void shouldNotFailIfFileNotFound() throws IOException {
- assertEquals("", new ServerImpl().loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/unknown-file.properties"));
+ assertEquals("", new ServerImpl(new PropertiesConfiguration()).loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/unknown-file.properties"));
}
}
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.server.platform;
+
+import org.apache.commons.lang.StringUtils;
+import org.hamcrest.core.Is;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import static org.hamcrest.text.StringStartsWith.startsWith;
+import static org.junit.Assert.*;
+
+public class ServerKeyGeneratorTest {
+
+ private static InetAddress localhost;
+
+ @BeforeClass
+ public static void init() throws UnknownHostException {
+ localhost = InetAddress.getLocalHost();
+ }
+
+ @Test
+ public void keyShouldHaveTenCharacters() {
+ ServerKeyGenerator.ServerKey key = new ServerKeyGenerator.ServerKey("SonarSource", localhost);
+ assertThat(key.getKey().length(), Is.is(10)); // first character is version + 9 characters for checksum
+ assertThat(StringUtils.isBlank(key.getKey()), Is.is(false));
+ }
+
+ @Test
+ public void keyShouldStartWithVersion() {
+ ServerKeyGenerator.ServerKey key = new ServerKeyGenerator.ServerKey("SonarSource", localhost);
+ assertThat(key.getKey(), startsWith(ServerKeyGenerator.VERSION));
+ }
+
+ @Test
+ public void loopbackAddressesShouldNotBeValid() throws UnknownHostException {
+ assertThat(new ServerKeyGenerator.ServerKey("SonarSource", InetAddress.getByName("127.0.0.1")).isValid(), Is.is(false));
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ ServerKeyGenerator.ServerKey key1 = new ServerKeyGenerator.ServerKey("Corp One", localhost);
+ ServerKeyGenerator.ServerKey key2 = new ServerKeyGenerator.ServerKey("Corp Two", localhost);
+ assertEquals(key1, key1);
+ assertEquals(key1.hashCode(), key1.hashCode());
+
+ assertThat(key1.equals(key2), Is.is(false));
+ assertThat(key2.equals(key1), Is.is(false));
+
+ assertThat(key1.equals("string"), Is.is(false));
+ }
+
+ @Test
+ public void shouldGenerateKey() {
+ String key = new ServerKeyGenerator().generate("SonarSource");
+ assertThat(StringUtils.isNotBlank(key), Is.is(true));
+ }
+
+ @Test
+ public void organizationShouldBeMandatory() {
+ assertNull(new ServerKeyGenerator().generate(null));
+ assertNull(new ServerKeyGenerator().generate(""));
+ assertNull(new ServerKeyGenerator().generate(" "));
+ }
+
+ @Test
+ public void keyShouldBeUniquePerOrganization() {
+ ServerKeyGenerator generator = new ServerKeyGenerator();
+ String k1 = generator.generate("Corp One");
+ String k2 = generator.generate("Corp Two");
+ assertThat(StringUtils.equals(k1, k2), Is.is(false));
+ }
+
+ @Test
+ public void keyShouldBeReproducible() {
+ ServerKeyGenerator generator = new ServerKeyGenerator();
+ String k1 = generator.generate("SonarSource");
+ String k2 = generator.generate("SonarSource");
+ assertThat(StringUtils.equals(k1, k2), Is.is(true));
+ }
+
+ @Test
+ public void shouldNotKeepPreviousKeyIfNotValid() {
+ ServerKeyGenerator generator = new ServerKeyGenerator();
+ String key = generator.generate("SonarSource", "unvalid");
+ assertNotNull(key);
+ assertThat(StringUtils.equals(key, "unvalid"), Is.is(false));
+ }
+
+
+}
public String getURL() {
return null;
}
+
+ public String getKey() {
+ return null;
+ }
}
*/
package org.sonar.server.startup;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
-import org.sonar.jpa.test.AbstractDbUnitTestCase;
import org.sonar.api.platform.Server;
-import org.sonar.server.platform.ServerImpl;
+import org.sonar.jpa.test.AbstractDbUnitTestCase;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
public class ServerMetadataPersisterTest extends AbstractDbUnitTestCase {
+ private TimeZone initialTimeZone;
+
+ @Before
+ public void fixTimeZone() {
+ initialTimeZone = TimeZone.getDefault();
+ TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
+ }
+
+ @After
+ public void revertTimeZone() {
+ TimeZone.setDefault(initialTimeZone);
+ }
+
@Test
public void testSaveProperties() throws ParseException {
setupData("testSaveProperties");
- persist();
+ persist(newServer());
checkTables("testSaveProperties", "properties");
}
@Test
public void testUpdateExistingProperties() throws ParseException {
setupData("testUpdateExistingProperties");
- persist();
+ persist(newServer());
checkTables("testUpdateExistingProperties", "properties");
}
- private void persist() throws ParseException {
- TimeZone initialZone = TimeZone.getDefault();
- try {
- TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
- Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm").parse("2010-05-18 17:59");
- Server server = new ServerImpl("123", "2.2", date);
- ServerMetadataPersister persister = new ServerMetadataPersister(server, getSession());
- persister.start();
- } finally {
- TimeZone.setDefault(initialZone);
- }
+ @Test
+ public void testDeleteProperties() throws ParseException {
+ setupData("testDeleteProperties");
+ Server server = mock(Server.class);
+ when(server.getStartedAt()).thenReturn(new SimpleDateFormat("yyyy-MM-dd HH:mm").parse("2010-05-18 17:59"));//this is a mandatory not-null property
+ persist(server);
+ checkTables("testDeleteProperties", "properties");
+ }
+
+ private void persist(Server server) {
+ ServerMetadataPersister persister = new ServerMetadataPersister(server, getSession());
+ persister.start();
+ }
+
+ private Server newServer() throws ParseException {
+ Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm").parse("2010-05-18 17:59");
+ Server server = mock(Server.class);
+ when(server.getKey()).thenReturn("1abcdef");
+ when(server.getId()).thenReturn("123");
+ when(server.getVersion()).thenReturn("2.2");
+ when(server.getStartedAt()).thenReturn(date);
+
+ return server;
+
}
}
--- /dev/null
+<dataset>
+
+ <properties id="1" prop_key="other" resource_id="[null]" text_value="some text" user_id="[null]"/>
+
+ <!-- not null property -->
+ <properties id="5" prop_key="sonar.core.startTime" resource_id="[null]" text_value="2010-05-18T17:59:00+0000" user_id="[null]"/>
+</dataset>
\ No newline at end of file
--- /dev/null
+<dataset>
+
+ <properties id="1" prop_key="other" resource_id="[null]" text_value="some text" user_id="[null]" />
+
+ <properties id="2" prop_key="sonar.core.id" resource_id="[null]" text_value="123" user_id="[null]"/>
+ <properties id="3" prop_key="sonar.core.version" resource_id="[null]" text_value="2.2" user_id="[null]"/>
+ <properties id="4" prop_key="sonar.serverKey.secured" resource_id="[null]" text_value="1abcdef" user_id="[null]"/>
+ <properties id="5" prop_key="sonar.core.startTime" resource_id="[null]" text_value="2010-05-18T17:59:00+0000" user_id="[null]"/>
+
+</dataset>
\ No newline at end of file
<properties id="2" prop_key="sonar.core.id" resource_id="[null]" text_value="123" user_id="[null]"/>
<properties id="3" prop_key="sonar.core.version" resource_id="[null]" text_value="2.2" user_id="[null]"/>
- <properties id="4" prop_key="sonar.core.startTime" resource_id="[null]" text_value="2010-05-18T17:59:00+0000" user_id="[null]"/>
+ <properties id="4" prop_key="sonar.serverKey.secured" resource_id="[null]" text_value="1abcdef" user_id="[null]"/>
+ <properties id="5" prop_key="sonar.core.startTime" resource_id="[null]" text_value="2010-05-18T17:59:00+0000" user_id="[null]"/>
</dataset>
\ No newline at end of file
<properties id="2" prop_key="sonar.core.id" resource_id="[null]" text_value="123" user_id="[null]"/>
<properties id="3" prop_key="sonar.core.version" resource_id="[null]" text_value="2.2" user_id="[null]"/>
<properties id="4" prop_key="sonar.core.startTime" resource_id="[null]" text_value="2010-05-18T17:59:00+0000" user_id="[null]"/>
-
+ <properties id="5" prop_key="sonar.serverKey.secured" resource_id="[null]" text_value="1abcdef" user_id="[null]"/>
</dataset>
\ No newline at end of file
<properties id="2" prop_key="sonar.core.id" resource_id="[null]" text_value="65" user_id="[null]"/>
<properties id="3" prop_key="sonar.core.version" resource_id="[null]" text_value="1.9" user_id="[null]"/>
<properties id="4" prop_key="sonar.core.startTime" resource_id="[null]" text_value="2008-04-18T17:59:00+0000" user_id="[null]"/>
+ <properties id="5" prop_key="sonar.serverKey.secured" resource_id="[null]" text_value="other" user_id="[null]"/>
</dataset>
\ No newline at end of file