/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.camel.component.consul;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.google.common.base.Optional;
import com.google.common.net.HostAndPort;
import com.orbitz.consul.Consul;
import com.orbitz.consul.ConsulException;
import com.orbitz.consul.KeyValueClient;
import com.orbitz.consul.SessionClient;
import com.orbitz.consul.model.session.ImmutableSession;
import com.orbitz.consul.model.session.SessionCreatedResponse;
import org.apache.camel.NoSuchBeanException;
import org.apache.camel.spi.Registry;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.SerializationUtils;
/**
* Apache Camel Plug-in for Consul Registry (Objects stored under kv/key as well
* as bookmarked under kv/[type]/key to avoid iteration over types)
*/
public class ConsulRegistry implements Registry {
private String hostname = "localhost";
private int port = 8500;
private Consul consul;
private KeyValueClient kvClient;
/* constructor with default port */
public ConsulRegistry(String hostname) {
this(hostname, 8500);
}
/* constructor (since spring.xml does not support builder pattern) */
public ConsulRegistry(String hostname, int port) {
super();
this.hostname = hostname;
this.port = port;
HostAndPort hostAndPort = HostAndPort.fromParts(hostname, port);
this.consul = Consul.builder().withHostAndPort(hostAndPort).build();
}
/* builder pattern */
private ConsulRegistry(Builder builder) {
this.hostname = builder.hostname;
this.port = builder.port;
HostAndPort hostAndPort = HostAndPort.fromParts(hostname, port);
this.consul = Consul.builder().withHostAndPort(hostAndPort).build();
}
@Override
public Object lookupByName(String key) {
// Substitute $ character in key
key = key.replaceAll("\\$", "/");
kvClient = consul.keyValueClient();
Optional<String> result = kvClient.getValueAsString(key);
if (result.isPresent()) {
byte[] postDecodedValue = Base64.decodeBase64(result.get());
return SerializationUtils.deserialize(postDecodedValue);
}
return null;
}
@Override
public <T> T lookupByNameAndType(String name, Class<T> type) {
Object object = lookupByName(name);
if (object == null) {
return null;
}
try {
return type.cast(object);
} catch (Throwable e) {
String msg = "Found bean: " + name + " in Consul Registry: " + this + " of type: "
+ object.getClass().getName() + "expected type was: " + type;
throw new NoSuchBeanException(name, msg, e);
}
}
@Override
public <T> Map<String, T> findByTypeWithName(Class<T> type) {
Object obj = null;
Map<String, T> result = new HashMap<String, T>();
// encode $ signs as they occur in subclass types
String keyPrefix = type.getName().replaceAll("\\$", "/");
kvClient = consul.keyValueClient();
List<String> keys;
try {
keys = kvClient.getKeys(keyPrefix);
} catch (ConsulException e) {
return result;
}
if (keys != null) {
for (String key : keys) {
// change bookmark back into actual key
key = key.substring(key.lastIndexOf('/') + 1);
obj = lookupByName(key.replaceAll("\\$", "/"));
if (type.isInstance(obj)) {
result.put(key, type.cast(obj));
}
}
}
return result;
}
@Override
public <T> Set<T> findByType(Class<T> type) {
String keyPrefix = type.getName().replaceAll("\\$", "/");
Object object = null;
Set<T> result = new HashSet<T>();
List<String> keys = null;
try {
keys = kvClient.getKeys(keyPrefix);
} catch (ConsulException e) {
return result;
}
if (keys != null) {
for (String key : keys) {
// change bookmark back into actual key
key = key.substring(key.lastIndexOf('/') + 1);
object = lookupByName(key.replaceAll("\\$", "/"));
if (type.isInstance(object)) {
result.add(type.cast(object));
}
}
}
return result;
}
public void remove(String key) {
// create session to avoid conflicts (not sure if that is safe enough)
SessionClient sessionClient = consul.sessionClient();
String sessionName = "session_" + UUID.randomUUID().toString();
SessionCreatedResponse response = sessionClient
.createSession(ImmutableSession.builder().name(sessionName).build());
String sessionId = response.getId();
kvClient = consul.keyValueClient();
String lockKey = "lock_" + key;
kvClient.acquireLock(lockKey, sessionName, sessionId);
Object object = lookupByName(key);
if (object == null) {
String msg = "Bean with key '" + key + "' did not exist in Consul Registry.";
throw new NoSuchBeanException(msg);
}
kvClient.deleteKey(key);
kvClient.deleteKey(object.getClass().getName() + "/" + key);
kvClient.releaseLock(lockKey, sessionId);
}
public void put(String key, Object object) {
// Substitute $ character in key
key = key.replaceAll("\\$", "/");
// create session to avoid conflicts
// (not sure if that is safe enough, again)
SessionClient sessionClient = consul.sessionClient();
String sessionName = "session_" + UUID.randomUUID().toString();
SessionCreatedResponse response = sessionClient
.createSession(ImmutableSession.builder().name(sessionName).build());
String sessionId = response.getId();
kvClient = consul.keyValueClient();
String lockKey = "lock_" + key;
kvClient.acquireLock(lockKey, sessionName, sessionId);
// Allow only unique keys, last one wins
if (lookupByName(key) != null) {
remove(key);
}
Object clone = SerializationUtils.clone((Serializable) object);
byte[] serializedObject = SerializationUtils.serialize((Serializable) clone);
// pre-encode due native encoding issues
byte[] preEncodedValue = Base64.encodeBase64(serializedObject);
String value = new String(preEncodedValue);
// store the actual class
kvClient.putValue(key, value);
// store just as a bookmark
kvClient.putValue(object.getClass().getName().replaceAll("\\$", "/") + "/" + key, "1");
kvClient.releaseLock(lockKey, sessionId);
}
public static class Builder {
// required parameter
String hostname;
// optional parameter
Integer port = 8500;
public Builder(String hostname) {
this.hostname = hostname;
}
public Builder port(Integer port) {
this.port = port;
return this;
}
public ConsulRegistry build() {
return new ConsulRegistry(this);
}
}
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
@Override
public Object lookup(String name) {
return lookupByName(name);
}
@Override
public <T> T lookup(String name, Class<T> type) {
return lookupByNameAndType(name, type);
}
@Override
public <T> Map<String, T> lookupByType(Class<T> type) {
return lookupByType(type);
}
}