Adds local file registry implementation.
MODIFY
gateway/engine/core/src/main/java/io/apiman/gateway/engine/impl/InMemoryRegistry.java
gateway/engine/core/src/main/java/io/apiman/gateway/engine/impl/InMemoryRegistry.java
@@ -324,7 +324,7 @@ public class InMemoryRegistry implements IRegistry {
* @param api an api
* @return a api key
*/
- private String getApiIndex(Api api) {
+ protected String getApiIndex(Api api) {
return getApiIndex(api.getOrganizationId(), api.getApiId(), api.getVersion());
}
@@ -346,7 +346,7 @@ public class InMemoryRegistry implements IRegistry {
* @param client an client
* @return a client key
*/
- private String getClientIndex(Client client) {
+ protected String getClientIndex(Client client) {
return getClientIndex(client.getOrganizationId(), client.getClientId(), client.getVersion());
}

ADD
gateway/engine/filesystem/README.md
+# Local file registry implementation
+
+Allows the Gateway to run in 'headless' mode, without an external backing store.
+
+### Use cases
+
+This is particularly useful when immutability is preferred, such as when using a GitOps approach.
+
+It is also particularly valuable when rolling deployments are required, where different instances of the gateway may be running at the same time with different, versioned, configurations, where pointing to a single backing store would be problematic.
+
+### How to use it
+
+Set the registry implementation and path to the registry:
+
+```
+apiman-gateway.registry=io.apiman.gateway.engine.filesystem.LocalFileRegistry
+apiman-gateway.registry.registry-path=/path/to/registry.json
+```
+
+The registry JSON can be created by using [apiman-cli](https://github.com/apiman/apiman-cli), for example;
+
+```
+./apiman gateway generate headless --declaration-file ./my-apis.yml --outputFile /path/to/registry.json
+```

ADD
gateway/engine/filesystem/pom.xml
+<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">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>io.apiman</groupId>
+ <artifactId>apiman-gateway-engine</artifactId>
+ <version>1.5.6-SNAPSHOT</version>
+ </parent>
+ <artifactId>apiman-gateway-engine-filesystem</artifactId>
+ <packaging>bundle</packaging>
+ <name>apiman-gateway-engine-filesystem</name>
+
+ <dependencies>
+ <!-- Project Dependencies -->
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apiman-common-util</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apiman-gateway-engine-beans</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apiman-gateway-engine-core</artifactId>
+ </dependency>
+
+ <!-- Third Party Dependencies -->
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
+
+ <!-- Test Only Libs -->
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apiman-test-common</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <artifactId>slf4j-log4j12</artifactId>
+ <groupId>org.slf4j</groupId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ </plugins>
+ </build>
+</project>

ADD
gateway/engine/filesystem/src/main/java/io/apiman/gateway/engine/filesystem/LocalFileRegistry.java
+package io.apiman.gateway.engine.filesystem;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.apiman.gateway.engine.async.IAsyncResultHandler;
+import io.apiman.gateway.engine.beans.Api;
+import io.apiman.gateway.engine.beans.Client;
+import io.apiman.gateway.engine.beans.exceptions.RegistrationException;
+import io.apiman.gateway.engine.filesystem.model.RegistryWrapper;
+import io.apiman.gateway.engine.impl.InMemoryRegistry;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * Adds local file based registry implementation.
+ *
+ * @author Pete Cornish
+ */
+public class LocalFileRegistry extends InMemoryRegistry {
+ static final String CONFIG_REGISTRY_PATH = "registry-path";
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private static final Object mutex = new Object();
+ private final File registryFile;
+
+ /**
+ * The cached version of the registry.
+ */
+ private Map<String, Object> map;
+
+ public LocalFileRegistry(Map<String, String> config) {
+ final String registryPath = config.get(CONFIG_REGISTRY_PATH);
+ if (StringUtils.isEmpty(registryPath)) {
+ throw new IllegalStateException("Registry path is not set");
+ } else {
+ registryFile = new File(registryPath);
+ }
+ }
+
+ /**
+ * Read the {@link #map} from the filesystem.
+ *
+ * @return the registry map
+ */
+ @Override
+ public Map<String, Object> getMap() {
+ if (null == map) {
+ synchronized (mutex) {
+ // double-guard
+ if (null == map) {
+ final Map<String, Object> registryMap = new ConcurrentHashMap<>();
+ if (registryFile.exists()) {
+ try (final InputStream in = FileUtils.openInputStream(registryFile)) {
+ final RegistryWrapper wrapper = mapper.readValue(in, RegistryWrapper.class);
+
+ registryMap.putAll(wrapper.getApis().stream()
+ .collect(toMap(this::getApiIndex, identity())));
+ registryMap.putAll(wrapper.getClients().stream()
+ .collect(toMap(this::getClientIndex, identity())));
+
+ } catch (Exception e) {
+ throw new RuntimeException("Error reading registry from file: " + registryFile, e);
+ }
+ }
+ map = registryMap;
+ }
+ }
+ }
+ return map;
+ }
+
+ /**
+ * Store the {@link #map} to the filesystem.
+ */
+ private void persist() {
+ synchronized (mutex) {
+ try (final OutputStream out = FileUtils.openOutputStream(registryFile)) {
+ final RegistryWrapper wrapper = new RegistryWrapper();
+ wrapper.getApis().addAll(filterByType(map, Api.class));
+ wrapper.getClients().addAll(filterByType(map, Client.class));
+
+ mapper.writeValue(out, wrapper);
+
+ } catch (Exception e) {
+ throw new RuntimeException("Error persisting registry to file: " + registryFile, e);
+ }
+ }
+ }
+
+ /**
+ * Clear the cached {@link #map}, forcing a reload from disk on next read.
+ */
+ void clear() {
+ synchronized (mutex) {
+ map = null;
+ }
+ }
+
+ private <V> Collection<V> filterByType(Map<String, ?> entries, Class<V> clazz) {
+ return entries.values().stream()
+ .filter(e -> clazz.isAssignableFrom(e.getClass()))
+ .map(clazz::cast)
+ .collect(toSet());
+ }
+
+ @Override
+ public void publishApi(Api api, IAsyncResultHandler<Void> handler) {
+ super.publishApi(api, handler);
+ persist();
+ }
+
+ @Override
+ public void retireApi(Api api, IAsyncResultHandler<Void> handler) {
+ super.retireApi(api, handler);
+ persist();
+ }
+
+ @Override
+ public void registerClient(Client client, IAsyncResultHandler<Void> handler) {
+ super.registerClient(client, handler);
+ persist();
+ }
+
+ @Override
+ public void unregisterClient(Client client, IAsyncResultHandler<Void> handler) {
+ super.unregisterClient(client, handler);
+ persist();
+ }
+
+ @Override
+ protected void unregisterClientInternal(Client client, boolean silent) throws RegistrationException {
+ super.unregisterClientInternal(client, silent);
+ persist();
+ }
+}

ADD
gateway/engine/filesystem/src/main/java/io/apiman/gateway/engine/filesystem/model/RegistryWrapper.java
+package io.apiman.gateway.engine.filesystem.model;
+
+import io.apiman.gateway.engine.beans.Api;
+import io.apiman.gateway.engine.beans.Client;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Pete Cornish
+ */
+public class RegistryWrapper {
+ private List<Api> apis = new ArrayList<>();
+ private List<Client> clients = new ArrayList<>();
+
+ public List<Api> getApis() {
+ return apis;
+ }
+
+ public void setApis(List<Api> apis) {
+ this.apis = apis;
+ }
+
+ public List<Client> getClients() {
+ return clients;
+ }
+
+ public void setClients(List<Client> clients) {
+ this.clients = clients;
+ }
+}

ADD
gateway/engine/filesystem/src/test/java/io/apiman/gateway/engine/filesystem/LocalFileRegistryTest.java
+package io.apiman.gateway.engine.filesystem;
+
+import io.apiman.gateway.engine.beans.Api;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static io.apiman.gateway.engine.filesystem.LocalFileRegistry.CONFIG_REGISTRY_PATH;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Pete Cornish
+ */
+public class LocalFileRegistryTest {
+ @Test
+ public void readWriteFileEmptyRegistry() throws Exception {
+ final File registryFile = Files.createTempFile("file-registry", ".json").toFile();
+
+ // don't start with a saved registry - just use the filename
+ //noinspection ResultOfMethodCallIgnored
+ registryFile.delete();
+
+ final Map<String, String> config = new HashMap<String, String>() {{
+ put(CONFIG_REGISTRY_PATH, registryFile.getAbsolutePath());
+ }};
+
+ final LocalFileRegistry registry = new LocalFileRegistry(config);
+ assertTrue("The map should be empty", registry.getMap().isEmpty());
+
+ final Api api = new Api() {{
+ setApiId("apiA");
+ setApiPolicies(Collections.emptyList());
+ setEndpoint("http://example.com");
+ setEndpointType("REST");
+ setOrganizationId("org");
+ setPublicAPI(true);
+ setVersion("1.0");
+ }};
+
+ registry.publishApi(api, result -> {
+ assertTrue("Publish should be successful", result.isSuccess());
+ assertFalse("The map should not be empty", registry.getMap().isEmpty());
+ });
+
+ // clear and re-read from file
+ registry.clear();
+
+ registry.getApi("org", "apiA", "1.0", result -> {
+ assertTrue(result.isSuccess());
+ assertNotNull(result.getResult());
+ assertEquals("apiA", result.getResult().getApiId());
+ });
+ }
+
+ @Test
+ public void readExistingRegistry() {
+ final Map<String, String> config = new HashMap<String, String>() {{
+ put(CONFIG_REGISTRY_PATH, LocalFileRegistryTest.class.getResource("/populated-registry.json").getPath());
+ }};
+
+ final LocalFileRegistry registry = new LocalFileRegistry(config);
+
+ assertFalse("The map should not be empty", registry.getMap().isEmpty());
+ assertNotNull("The API should exist in the map", registry.getMap().get("API::org|apiB|1.0"));
+
+ registry.getApi("org", "apiB", "1.0", result -> {
+ assertTrue(result.isSuccess());
+ assertNotNull(result.getResult());
+ assertEquals("apiB", result.getResult().getApiId());
+ });
+ }
+}

ADD
gateway/engine/filesystem/src/test/resources/populated-registry.json
+{
+ "apis": [
+ {
+ "publicAPI": true,
+ "organizationId": "org",
+ "apiId": "apiB",
+ "version": "1.0",
+ "endpoint": "http://example.com",
+ "endpointType": "REST",
+ "endpointContentType": null,
+ "endpointProperties": {},
+ "parsePayload": false,
+ "apiPolicies": [],
+ "maxPayloadBufferSize": 0
+ }
+ ],
+ "clients": []
+}

MODIFY
gateway/engine/pom.xml
gateway/engine/pom.xml
<module>beans</module>
<module>core</module>
<module>es</module>
+ <module>filesystem</module>
<module>hawkular</module>
<module>hazelcast</module>
<module>influxdb</module>

MODIFY
gateway/platforms/war/pom.xml
gateway/platforms/war/pom.xml
<groupId>${project.groupId}</groupId>
<artifactId>apiman-gateway-engine-core</artifactId>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apiman-gateway-engine-filesystem</artifactId>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apiman-gateway-engine-hazelcast</artifactId>

MODIFY
pom.xml
pom.xml
<artifactId>apiman-gateway-engine-core</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apiman-gateway-engine-filesystem</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apiman-gateway-engine-hazelcast</artifactId>

Adds local file registry implementation. (#844)