/**
* Copyright 2016 Hortonworks.
*
* 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 com.hortonworks.registries.storage.impl.memory;
import com.google.common.collect.Lists;
import com.hortonworks.registries.common.QueryParam;
import com.hortonworks.registries.common.util.ReflectionHelper;
import com.hortonworks.registries.storage.OrderByField;
import com.hortonworks.registries.storage.PrimaryKey;
import com.hortonworks.registries.storage.Storable;
import com.hortonworks.registries.storage.StorableKey;
import com.hortonworks.registries.storage.StorageManager;
import com.hortonworks.registries.storage.exception.AlreadyExistsException;
import com.hortonworks.registries.storage.exception.StorageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
//TODO: The synchronization is broken right now, so all the methods don't guarantee the semantics as described in the interface.
public class InMemoryStorageManager implements StorageManager {
private static final Logger LOG = LoggerFactory.getLogger(InMemoryStorageManager.class);
private final ConcurrentHashMap<String, ConcurrentHashMap<PrimaryKey, Storable>> storageMap = new ConcurrentHashMap<String, ConcurrentHashMap<PrimaryKey, Storable>>();
private final ConcurrentHashMap<String, AtomicLong> sequenceMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Class<?>> nameSpaceClassMap = new ConcurrentHashMap<String, Class<?>>();
@Override
public void init(Map<String, Object> properties) {
}
@Override
public void add(Storable storable) throws AlreadyExistsException {
final Storable existing = get(storable.getStorableKey());
if (existing == null) {
addOrUpdate(storable);
} else if (!existing.equals(storable)) {
throw new AlreadyExistsException("Another instance with same id = " + storable.getPrimaryKey()
+ " exists with different value in namespace " + storable.getNameSpace()
+ " Consider using addOrUpdate method if you always want to overwrite.");
}
}
@Override
public <T extends Storable> T remove(StorableKey key) throws StorageException {
if (storageMap.containsKey(key.getNameSpace())) {
return (T) storageMap.get(key.getNameSpace()).remove(key.getPrimaryKey());
}
return null;
}
@Override
public void addOrUpdate(Storable storable) {
String namespace = storable.getNameSpace();
PrimaryKey id = storable.getPrimaryKey();
if (!storageMap.containsKey(namespace)) {
storageMap.putIfAbsent(namespace, new ConcurrentHashMap<PrimaryKey, Storable>());
nameSpaceClassMap.putIfAbsent(namespace, storable.getClass());
}
if (!storageMap.get(namespace).containsKey(id)) {
nextId(namespace);
}
storageMap.get(namespace).put(id, storable);
}
@Override
public <T extends Storable> T get(StorableKey key) throws StorageException {
return storageMap.containsKey(key.getNameSpace())
? (T) storageMap.get(key.getNameSpace()).get(key.getPrimaryKey())
: null;
}
/**
* Uses reflection to query the field or the method. Assumes
* a public getXXX method is available to get the field value.
*/
private boolean matches(Storable val, List<QueryParam> queryParams) {
Object fieldValue;
boolean res = true;
for (QueryParam qp : queryParams) {
try {
fieldValue = ReflectionHelper.invokeGetter(qp.name, val);
if (!fieldValue.toString().equals(qp.value)) {
return false;
}
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
LOG.error("FAILED to invoke getter for query param {} , is your param name correct?", qp.getName(), e);
return false;
}
}
return res;
}
public <T extends Storable> Collection<T> find(final String namespace,
final List<QueryParam> queryParams) throws StorageException {
return find(namespace, queryParams, Collections.emptyList());
}
@Override
public <T extends Storable> Collection<T> find(final String namespace,
final List<QueryParam> queryParams,
final List<OrderByField> orderByFields) throws StorageException {
List<T> storables = new ArrayList<>();
if (queryParams == null) {
Collection<T> collection = list(namespace);
storables = Lists.newArrayList(collection);
} else {
Class<?> clazz = nameSpaceClassMap.get(namespace);
if (clazz != null) {
Map<PrimaryKey, Storable> storableMap = storageMap.get(namespace);
if (storableMap != null) {
for (Storable val : storableMap.values()) {
if (matches(val, queryParams)) {
storables.add((T) val);
}
}
}
}
}
if (orderByFields != null && !orderByFields.isEmpty()) {
storables.sort((storable1, storable2) -> {
try {
for (OrderByField orderByField : orderByFields) {
Comparable value1 = ReflectionHelper.invokeGetter(orderByField.getFieldName(), storable1);
Comparable value2 = ReflectionHelper.invokeGetter(orderByField.getFieldName(), storable2);
int compareTo;
// same values continue
if(value1 == value2) {
continue;
} else if(value1 == null) {
// value2 is non null
compareTo = -1;
} else if(value2 == null) {
// value1 is non null
compareTo = 1;
} else {
// both value and value2 non null
compareTo = value1.compareTo(value2);
}
if (compareTo == 0) {
continue;
}
return orderByField.isDescending() ? -compareTo : compareTo;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
// all group by fields are matched means equal
return 0;
});
}
return storables;
}
@Override
public <T extends Storable> Collection<T> list(String namespace) throws StorageException {
return storageMap.containsKey(namespace)
? (Collection<T>) storageMap.get(namespace).values() : Collections.<T>emptyList();
}
@Override
public void cleanup() throws StorageException {
//no-op
}
/**
* atomically increment and return the next id for the given namespace
*/
@Override
public Long nextId(String namespace) {
AtomicLong cur = sequenceMap.get(namespace);
if (cur == null) {
AtomicLong zero = new AtomicLong();
cur = sequenceMap.putIfAbsent(namespace, zero);
if (cur == null) {
cur = zero;
}
}
return cur.incrementAndGet();
}
@Override
public void registerStorables(Collection<Class<? extends Storable>> classes) throws StorageException {
}
}