Commit ea931248 by Johannes Stelzer

Redesign Registry

* Separate AppStore from AppRegistry * Allow Id generation to be altered * Remove logic from Application; Move id genretation into AppRegistry * Make AppStore threadSafe
parent 75afae26
......@@ -25,8 +25,11 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import de.codecentric.boot.admin.controller.RegistryController;
import de.codecentric.boot.admin.service.ApplicationRegistry;
import de.codecentric.boot.admin.service.SimpleApplicationRegistry;
import de.codecentric.boot.admin.registry.ApplicationIdGenerator;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.registry.HashingApplicationUrlIdGenerator;
import de.codecentric.boot.admin.registry.store.ApplicationStore;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
@Configuration
public class WebappConfig extends WebMvcConfigurerAdapter {
......@@ -52,8 +55,27 @@ public class WebappConfig extends WebMvcConfigurerAdapter {
*/
@Bean
@ConditionalOnMissingBean
public ApplicationRegistry applicationRegistry() {
return new SimpleApplicationRegistry();
public ApplicationRegistry applicationRegistry(ApplicationStore applicationStore,
ApplicationIdGenerator applicationIdGenerator) {
return new ApplicationRegistry(applicationStore, applicationIdGenerator);
}
/**
* Default applicationId Generator
*/
@Bean
@ConditionalOnMissingBean
public HashingApplicationUrlIdGenerator applicationIdGenerator() {
return new HashingApplicationUrlIdGenerator();
}
/**
* Default applicationId Generator
*/
@Bean
@ConditionalOnMissingBean
public ApplicationStore applicationStore() {
return new SimpleApplicationStore();
}
}
......@@ -28,7 +28,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.service.ApplicationRegistry;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.registry.ApplicationRegistryConflictException;
/**
* REST controller for controlling registration of managed applications.
......@@ -53,14 +54,10 @@ public class RegistryController {
@RequestMapping(value = "/api/applications", method = RequestMethod.POST)
public ResponseEntity<Application> register(@RequestBody Application app) {
LOGGER.debug("Register application {}", app.toString());
Application registered = registry.getApplication(app.getId());
if (registered == null || registered.equals(app)) {
LOGGER.info("Application {} registered.", app.toString());
registry.register(app);
return new ResponseEntity<Application>(app, HttpStatus.CREATED);
}
else {
try {
Application registeredApp = registry.register(app);
return new ResponseEntity<Application>(registeredApp, HttpStatus.CREATED);
} catch (ApplicationRegistryConflictException ex) {
return new ResponseEntity<Application>(HttpStatus.CONFLICT);
}
}
......@@ -77,8 +74,7 @@ public class RegistryController {
Application application = registry.getApplication(id);
if (application != null) {
return new ResponseEntity<Application>(application, HttpStatus.OK);
}
else {
} else {
return new ResponseEntity<Application>(application, HttpStatus.NOT_FOUND);
}
}
......@@ -90,12 +86,11 @@ public class RegistryController {
*/
@RequestMapping(value = "/api/application/{id}", method = RequestMethod.DELETE)
public ResponseEntity<Application> unregister(@PathVariable String id) {
LOGGER.info("Unregister application with ID '{}'", id);
LOGGER.debug("Unregister application with ID '{}'", id);
Application app = registry.unregister(id);
if (app != null) {
return new ResponseEntity<Application>(app, HttpStatus.NO_CONTENT);
}
else {
} else {
return new ResponseEntity<Application>(HttpStatus.NOT_FOUND);
}
}
......
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.registry;
import de.codecentric.boot.admin.model.Application;
public interface ApplicationIdGenerator {
/**
* Generate an id based on the given Application
*
* @param a the application the id is computed for.
* @return the application id
*/
String generateId(Application a);
}
......@@ -13,43 +13,67 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.service;
package de.codecentric.boot.admin.registry;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.Validate;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.registry.store.ApplicationStore;
/**
* This registry is just "in-memory", so that after a restart all applications have to be
* registered again.
* Registry for all applications that should be managed/administrated by the Spring Boot Admin application.
* Backed by an ApplicationStore for persistence and an ApplicationIdGenerator for id generation.
*/
@Service
public class SimpleApplicationRegistry implements ApplicationRegistry {
public class ApplicationRegistry {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationRegistry.class);
private final Map<String, Application> store = new HashMap<>();
private final ApplicationStore store;
private final ApplicationIdGenerator generator;
@Override
public void register(Application app) {
public ApplicationRegistry(ApplicationStore store, ApplicationIdGenerator generator) {
this.store = store;
this.generator = generator;
}
/**
* Register application.
*
* @param app The Application.
*/
public Application register(Application app) {
Validate.notNull(app, "Application must not be null");
Validate.notNull(app.getId(), "ID must not be null");
Validate.notNull(app.getUrl(), "URL must not be null");
Validate.isTrue(checkUrl(app.getUrl()), "URL is not valid");
store.put(app.getId(), app);
String applicationId = generator.generateId(app);
Validate.notNull(applicationId, "ID must not be null");
Application newApp = new Application(app.getUrl(), app.getName(), applicationId);
Application oldApp = store.put(newApp);
if (oldApp == null) {
LOGGER.info("New Application {} registered ", newApp);
} else {
if ((app.getUrl().equals(oldApp.getUrl()) && app.getName().equals(oldApp.getName()))) {
LOGGER.info("Application {} refreshed", newApp);
} else {
LOGGER.warn("Application {} not registered because of conflict with {}", newApp, oldApp);
throw new ApplicationRegistryConflictException(oldApp, app);
}
}
return newApp;
}
/**
* Checks the syntax of the given URL.
*
* @param url
* The URL.
* @param url The URL.
* @return true, if valid.
*/
private boolean checkUrl(String url) {
......@@ -61,19 +85,34 @@ public class SimpleApplicationRegistry implements ApplicationRegistry {
return true;
}
@Override
/**
* Get a list of all registered applications.
*
* @return List.
*/
public List<Application> getApplications() {
return new ArrayList<>(store.values());
return store.getAll();
}
@Override
/**
* Get a specific application inside the registry.
*
* @param id Id.
* @return Application.
*/
public Application getApplication(String id) {
return store.get(id);
}
@Override
/**
* Remove a specific application from registry
*
* @param id
* @return the unregistered Application
*/
public Application unregister(String id) {
return store.remove(id);
Application app = store.remove(id);
LOGGER.info("Application {} unregistered ", app);
return app;
}
}
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.registry;
import de.codecentric.boot.admin.model.Application;
public class ApplicationRegistryConflictException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ApplicationRegistryConflictException(Application oldApp, Application newApp) {
super("Conflict in ApplicationRegistry: " + oldApp.toString() + " vs. " + newApp.toString());
}
}
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.registry;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import de.codecentric.boot.admin.model.Application;
/**
* Generates an SHA-1 Hash based on the applications url.
*/
public class HashingApplicationUrlIdGenerator implements ApplicationIdGenerator {
private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
'e', 'f' };
@Override
public String generateId(Application a) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = digest.digest(a.getUrl().getBytes(Charset.forName("UTF-8")));
return new String(encodeHex(bytes, 0, 8));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private char[] encodeHex(byte[] bytes, int offset, int length) {
char chars[] = new char[length];
for (int i = 0; i < length; i = i + 2) {
byte b = bytes[offset + (i / 2)];
chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf];
chars[i + 1] = HEX_CHARS[b & 0xf];
}
return chars;
}
}
/*
* Copyright 2014 the original author or authors.
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -13,45 +13,39 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.service;
package de.codecentric.boot.admin.registry.store;
import java.util.List;
import de.codecentric.boot.admin.model.Application;
/**
* Registry for all applications that should be managed/administrated by the
* spring-boot-admin application.
* Responsible for storing applications.
*/
public interface ApplicationRegistry {
public interface ApplicationStore {
/**
* Register application.
* Inserts a new Application into the store. If the Id is already present in the store the Application is NOT stored.
*
* @param app The Application.
* @param app Application to store
* @return the Application associated previosly with the applications id.
*/
void register(Application app);
Application put(Application app);
/**
* Get a list of all registered applications.
*
* @return List.
* @return all Applications in the store;
*/
List<Application> getApplications();
List<Application> getAll();
/**
* Get a specific application inside the registry.
*
* @param id Id.
* @return Application.
* @param id the applications id
* @return the Application with the specified id;
*/
Application getApplication(String id);
Application get(String id);
/**
* Remove a specific application from registry
* @param id
* @return the unregistered Application
* @param id id of the Application to be removed
* @return the Application associated previosly with the applications id.
*/
Application unregister(String id);
Application remove(String id);
}
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.registry.store;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import de.codecentric.boot.admin.model.Application;
/**
* Simple ApplicationStore backed by a ConcurrentHashMap.
*/
public class SimpleApplicationStore implements ApplicationStore {
private final ConcurrentHashMap<String, Application> map = new ConcurrentHashMap<>();
@Override
public Application put(Application app) {
return map.putIfAbsent(app.getId(), app);
}
@Override
public List<Application> getAll() {
return new ArrayList<Application>(map.values());
}
@Override
public Application get(String id) {
return map.get(id);
}
@Override
public Application remove(String id) {
return map.remove(id);
}
}
......@@ -17,7 +17,6 @@ package de.codecentric.boot.admin.controller;
import static org.junit.Assert.assertEquals;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
......@@ -26,7 +25,9 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.service.SimpleApplicationRegistry;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.registry.HashingApplicationUrlIdGenerator;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
public class RegistryControllerTest {
......@@ -34,25 +35,27 @@ public class RegistryControllerTest {
@Before
public void setup() {
controller = new RegistryController(new SimpleApplicationRegistry());
controller = new RegistryController(new ApplicationRegistry(new SimpleApplicationStore(),
new HashingApplicationUrlIdGenerator()));
}
@Test
public void register() {
ResponseEntity<?> response = controller.register(new Application("http://localhost", "test"));
ResponseEntity<Application> response = controller.register(new Application("http://localhost", "test"));
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertEquals(new Application("http://localhost", "test"), response.getBody());
assertEquals("http://localhost", response.getBody().getUrl());
assertEquals("test", response.getBody().getName());
}
@Test
public void register_twice() {
controller.register(new Application("http://localhost", "test"));
Application app = new Application("http://localhost", "test");
ResponseEntity<?> response = controller.register(app);
ResponseEntity<Application> response = controller.register(app);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertEquals(new Application("http://localhost", "test"), response.getBody());
assertEquals("http://localhost", response.getBody().getUrl());
assertEquals("test", response.getBody().getName());
}
@Test
......@@ -65,11 +68,12 @@ public class RegistryControllerTest {
@Test
public void get() {
Application app = new Application("http://localhost", "FOO");
controller.register(app);
app = controller.register(app).getBody();
ResponseEntity<?> response = controller.get(app.getId());
ResponseEntity<Application> response = controller.get(app.getId());
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(app, response.getBody());
assertEquals("http://localhost", response.getBody().getUrl());
assertEquals("FOO", response.getBody().getName());
}
@Test
......@@ -80,11 +84,10 @@ public class RegistryControllerTest {
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}
@Test
public void unregister() {
Application app = new Application("http://localhost", "FOO");
controller.register(app);
app = controller.register(app).getBody();
ResponseEntity<?> response = controller.unregister(app.getId());
assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
......@@ -107,7 +110,9 @@ public class RegistryControllerTest {
controller.register(app);
List<Application> applications = controller.applications();
assertEquals(Collections.singletonList(app), applications);
assertEquals(1, applications.size());
assertEquals(app.getName(), applications.get(0).getName());
assertEquals(app.getUrl(), applications.get(0).getUrl());
}
}
......@@ -13,17 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.service;
package de.codecentric.boot.admin.registry;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.registry.HashingApplicationUrlIdGenerator;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
public class SimpleApplicationRegistryTest {
public class ApplicationRegistryTest {
private ApplicationRegistry registry = new SimpleApplicationRegistry();
private ApplicationRegistry registry = new ApplicationRegistry(new SimpleApplicationStore(),
new HashingApplicationUrlIdGenerator());
@Test(expected = NullPointerException.class)
public void registerFailed1() throws Exception {
......@@ -45,14 +50,17 @@ public class SimpleApplicationRegistryTest {
@Test
public void register() throws Exception {
Application app = new Application("http://localhost:8080", "abc");
registry.register(app);
}
Application response = registry.register(app);
assertEquals("http://localhost:8080", response.getUrl());
assertEquals("abc", response.getName());
assertNotNull(response.getId());
}
@Test
public void getApplication() throws Exception {
Application app = new Application("http://localhost:8080", "abc");
registry.register(app);
app = registry.register(app);
assertEquals(app, registry.getApplication(app.getId()));
}
......@@ -60,10 +68,10 @@ public class SimpleApplicationRegistryTest {
@Test
public void getApplications() throws Exception {
Application app = new Application("http://localhost:8080", "abc");
registry.register(app);
app = registry.register(app);
assertEquals(1, registry.getApplications().size());
assertEquals(app, registry.getApplications().get(0));
assertEquals("http://localhost:8080", registry.getApplications().get(0).getUrl());
assertEquals("abc", registry.getApplications().get(0).getName());
}
}
......@@ -16,8 +16,6 @@
package de.codecentric.boot.admin.model;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
......@@ -34,13 +32,14 @@ public class Application implements Serializable {
private final String url;
private final String name;
@JsonCreator
public Application(@JsonProperty("url") String url, @JsonProperty("name") String name) {
this(url.replaceFirst("/+$", ""), name, generateId(url.replaceFirst("/+$", "")));
public Application(String url, String name) {
this(url, name, null);
}
protected Application(String url, String name, String id) {
this.url = url;
@JsonCreator
public Application(@JsonProperty(value = "url", required = true) String url,
@JsonProperty(value = "name", required = true) String name, @JsonProperty("id") String id) {
this.url = url.replaceFirst("/+$", "");
this.name = name;
this.id = id;
}
......@@ -66,6 +65,7 @@ public class Application implements Serializable {
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((url == null) ? 0 : url.hashCode());
return result;
......@@ -83,46 +83,28 @@ public class Application implements Serializable {
return false;
}
Application other = (Application) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
}
else if (!name.equals(other.name)) {
} else if (!name.equals(other.name)) {
return false;
}
if (url == null) {
if (other.url != null) {
return false;
}
}
else if (!url.equals(other.url)) {
} else if (!url.equals(other.url)) {
return false;
}
return true;
}
private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
'e', 'f' };
private static String generateId(String url) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = digest.digest(url.getBytes(Charset.forName("UTF-8")));
return new String(encodeHex(bytes, 0, 8));
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
private static char[] encodeHex(byte[] bytes, int offset, int length) {
char chars[] = new char[length];
for (int i = 0; i < length; i = i + 2) {
byte b = bytes[offset + (i / 2)];
chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf];
chars[i + 1] = HEX_CHARS[b & 0xf];
}
return chars;
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.model;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import org.junit.Test;
public class ApplicationTest {
@Test
public void url_id() {
Application a = new Application("http://localhost:8080/", null);
Application b = new Application("http://localhost:8080", null);
assertEquals("same urls must have same id", a.getId(), b.getId());
Application z = new Application("http://127.0.0.1:8080", null);
assertFalse("different urls must have diffenrent Id", a.getId().equals(z.getId()));
}
public void equals() {
Application a = new Application("http://localhost:8080/", "FOO");
Application b = new Application("http://localhost:8080", "FOO");
assertEquals("same url and same name must be equals", a, b);
assertEquals("hashcode should be equals", a.hashCode(), b.hashCode());
Application z = new Application("http://127.0.0.1:8080", "FOO");
assertFalse("different urls same name must not be equals", a.equals(z));
Application y = new Application("http://localhost:8080", "BAR");
assertFalse("same urls different name must not be equals", a.getId().equals(y.getId()));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment