/**
* 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 org.deephacks.confit.internal.mapdb;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import org.deephacks.confit.admin.query.BeanQuery;
import org.deephacks.confit.internal.mapdb.query.DefaultBeanQuery;
import org.deephacks.confit.model.AbortRuntimeException;
import org.deephacks.confit.model.Bean;
import org.deephacks.confit.model.BeanId;
import org.deephacks.confit.model.Events;
import org.deephacks.confit.model.Schema;
import org.deephacks.confit.spi.BeanManager;
import org.deephacks.confit.spi.Lookup;
import org.mapdb.TxMaker;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.deephacks.confit.model.Events.*;
public class MapdbBeanManager extends BeanManager {
private final MapDB mapDB;
public MapdbBeanManager() {
TxMaker txMaker = Lookup.get().lookup(TxMaker.class);
Preconditions.checkNotNull(txMaker);
mapDB = new MapDB(txMaker);
}
@Override
public Optional<Bean> getEager(BeanId id) {
try {
Optional<Bean> bean = getEagerly(id);
mapDB.commit();
return bean;
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
private Optional<Bean> getEagerly(BeanId id) {
HashMap<BeanId, Bean> found = new HashMap<>();
getEagerly(id, found, new HashSet<BeanId>());
Bean bean = found.get(id);
if (bean == null) {
return Optional.absent();
}
return Optional.of(bean);
}
private void getEagerly(BeanId id, HashMap<BeanId, Bean> found, Set<BeanId> seen) {
if (seen.contains(id)) {
return;
}
seen.add(id);
Bean bean = found.get(id) == null ? mapDB.get(id) : found.get(id);
if (bean == null) {
return;
}
found.put(id, bean);
// bean found, fetch references.
for (BeanId ref : bean.getReferences()) {
if (ref.getBean() != null) {
continue;
}
Bean refBean = found.get(ref) == null ? mapDB.get(ref) : found.get(ref);
if (refBean == null) {
throw CFG301_MISSING_RUNTIME_REF(bean.getId(), ref);
}
ref.setBean(refBean);
found.put(ref, refBean);
getEagerly(ref, found, seen);
}
}
@Override
public Optional<Bean> getLazy(BeanId id) throws AbortRuntimeException {
try {
Bean bean = mapDB.get(id);
if (bean == null) {
return Optional.absent();
}
for (BeanId ref : bean.getReferences()) {
Bean refBean = mapDB.get(ref);
if (refBean == null) {
throw CFG301_MISSING_RUNTIME_REF(ref);
}
ref.setBean(refBean);
}
mapDB.commit();
return Optional.of(bean);
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
/**
* The direct, but no further, successors that references this bean will also be
* fetched and initialized with their direct, but no further, predecessors.
*/
@Override
public Map<BeanId, Bean> getBeanToValidate(Collection<Bean> beans) throws AbortRuntimeException {
try {
Map<BeanId, Bean> beansToValidate = new HashMap<>();
for (Bean bean : beans) {
Map<BeanId, Bean> predecessors = new HashMap<>();
// beans read from beansStorage will only have their basic properties initialized...
// ... but we also need set the direct references/predecessors for beans to validate
Map<BeanId, Bean> beansToValidateSubset = getDirectSuccessors(bean);
beansToValidateSubset.put(bean.getId(), bean);
for (Bean toValidate : beansToValidateSubset.values()) {
predecessors.putAll(getDirectPredecessors(toValidate));
}
for (Bean predecessor : predecessors.values()) {
for (BeanId ref : predecessor.getReferences()) {
Bean b = mapDB.get(ref);
if (b == null) {
throw CFG301_MISSING_RUNTIME_REF(predecessor.getId());
}
ref.setBean(b);
}
}
for (Bean toValidate : beansToValidateSubset.values()) {
// list references of beansToValidate should now
// be available in predecessors.
for (BeanId ref : toValidate.getReferences()) {
Bean predecessor = predecessors.get(ref);
if (predecessor == null) {
throw new IllegalStateException("Bug in algorithm. Reference [" + ref
+ "] of [" + toValidate.getId()
+ "] should be available in predecessors.");
}
ref.setBean(predecessor);
}
}
beansToValidate.putAll(predecessors);
}
mapDB.commit();
return beansToValidate;
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
private Map<BeanId, Bean> getDirectPredecessors(Bean bean) {
Map<BeanId, Bean> predecessors = new HashMap<>();
for (BeanId ref : bean.getReferences()) {
Bean predecessor = mapDB.get(ref);
if (predecessor == null) {
throw CFG304_BEAN_DOESNT_EXIST(ref);
}
predecessors.put(predecessor.getId(), predecessor);
}
return predecessors;
}
private Map<BeanId, Bean> getDirectSuccessors(Bean bean) {
Map<BeanId, Bean> successors = new HashMap<>();
for (BeanId id : bean.getReferences()) {
Bean successor = mapDB.get(id);
if (successor == null) {
throw Events.CFG301_MISSING_RUNTIME_REF(id);
}
successors.put(successor.getId(), successor);
}
return successors;
}
@Override
public Optional<Bean> getSingleton(String schemaName) throws IllegalArgumentException {
try {
Optional<Bean> bean = getEagerly(BeanId.createSingleton(schemaName));
if (bean.isPresent()) {
return bean;
}
mapDB.commit();
return Optional.of(Bean.create(BeanId.createSingleton(schemaName)));
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public Map<BeanId, Bean> list(String name) {
try {
Map<BeanId, Bean> result = new HashMap<>();
for (Bean b : mapDB.list(name)) {
if (b.getId().getSchemaName().equals(name)) {
Optional<Bean> bean = getEagerly(b.getId());
result.put(bean.get().getId(), bean.get());
}
}
mapDB.commit();
return result;
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public Map<BeanId, Bean> list(String schemaName, Collection<String> ids)
throws AbortRuntimeException {
try {
Map<BeanId, Bean> result = new HashMap<>();
for (Bean bean : mapDB.list(schemaName)) {
for (String id : ids) {
if (bean.getId().getInstanceId().equals(id)) {
result.put(bean.getId(), bean);
}
}
}
mapDB.commit();
return result;
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public void create(Bean bean) {
try {
checkUniquness(bean);
checkReferencesExist(bean, new ArrayList<Bean>());
mapDB.put(bean);
mapDB.commit();
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public void create(Collection<Bean> set) {
try {
// first check uniqueness towards beansStorage
for (Bean bean : set) {
checkUniquness(bean);
}
// references may not exist in beansStorage, but are provided
// as part of the transactions, so add them before validating references.
for (Bean bean : set) {
checkReferencesExist(bean, set);
}
for (Bean bean : set) {
mapDB.put(bean);
}
mapDB.commit();
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public void createSingleton(BeanId singleton) {
try {
Bean bean = Bean.create(singleton);
try {
checkUniquness(bean);
} catch (AbortRuntimeException e) {
// ignore and return silently.
return;
}
mapDB.put(bean);
mapDB.commit();
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public void set(final Bean bean) {
try {
Bean existing = mapDB.get(bean.getId());
if (existing == null) {
throw CFG304_BEAN_DOESNT_EXIST(bean.getId());
}
checkReferencesExist(bean, new ArrayList<Bean>());
mapDB.put(bean);
mapDB.commit();
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public void set(Collection<Bean> set) {
try {
// references may not exist in beansStorage, but are provided
// as part of the transactions, so add them before validating references.
for (Bean bean : set) {
Bean existing = mapDB.get(bean.getId());
if (existing == null) {
throw CFG304_BEAN_DOESNT_EXIST(bean.getId());
}
mapDB.put(bean);
}
for (Bean bean : set) {
checkReferencesExist(bean, set);
}
mapDB.commit();
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public void merge(Bean bean) {
try {
Bean b = mapDB.get(bean.getId());
if (b == null) {
throw CFG304_BEAN_DOESNT_EXIST(bean.getId());
}
replace(b, bean);
mapDB.commit();
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public void merge(Collection<Bean> bean) {
try {
for (Bean replace : bean) {
Bean target = mapDB.get(replace.getId());
if (target == null) {
throw Events.CFG304_BEAN_DOESNT_EXIST(replace.getId());
}
replace(target, replace);
mapDB.put(target);
}
mapDB.commit();
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
private void replace(Bean target, Bean replace) {
if (target == null) {
// bean did not exist in beansStorage, create it.
target = replace;
}
checkReferencesExist(replace, new ArrayList<Bean>());
for (String name : replace.getPropertyNames()) {
List<String> values = replace.getValues(name);
if (values == null || values.size() == 0) {
// null/empty indicates a remove/reset-to-default op
target.remove(name);
} else {
target.setProperty(name, replace.getValues(name));
}
}
for (String name : replace.getReferenceNames()) {
List<BeanId> values = replace.getReference(name);
if (values == null || values.size() == 0) {
// null/empty indicates a remove/reset-to-default op
target.remove(name);
} else {
target.setReferences(name, values);
}
}
}
@Override
public Bean delete(BeanId id) {
try {
checkNoReferencesExist(id);
Bean bean = mapDB.remove(id);
mapDB.commit();
return bean;
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public Collection<Bean> delete(String schemaName, Collection<String> instanceIds) {
try {
Collection<Bean> deleted = new ArrayList<>();
for (String instance : instanceIds) {
checkNoReferencesExist(BeanId.create(instance, schemaName));
BeanId id = BeanId.create(instance, schemaName);
if (mapDB.get(id) == null) {
throw Events.CFG304_BEAN_DOESNT_EXIST(id);
}
}
for (String instance : instanceIds) {
BeanId id = BeanId.create(instance, schemaName);
mapDB.remove(id);
}
mapDB.commit();
return deleted;
} catch (AbortRuntimeException e) {
mapDB.rollback(e);
throw e;
} catch (Throwable e) {
mapDB.rollback(e);
throw new RuntimeException(e);
}
}
@Override
public BeanQuery newQuery(Schema schema) {
LinkedHashMap<BeanId, byte[]> sorted = mapDB.listBinary(schema.getName());
return new DefaultBeanQuery(schema, sorted);
}
private void checkNoReferencesExist(BeanId deleted) {
Collection<BeanId> hasReferences = new ArrayList<>();
for (Bean b : mapDB.values()) {
if (hasReferences(b, deleted)) {
hasReferences.add(b.getId());
}
}
if (hasReferences.size() > 0) {
throw CFG302_CANNOT_DELETE_BEAN(Arrays.asList(deleted));
}
}
private void checkReferencesExist(final Bean bean, Collection<Bean> inflight) {
HashMap<BeanId, Bean> inflightBeans = new HashMap<>();
for (Bean b : inflight) {
inflightBeans.put(b.getId(), b);
}
ArrayList<BeanId> allRefs = new ArrayList<>();
for (String name : bean.getReferenceNames()) {
if (bean.getReference(name) == null) {
// the reference is about to be removed.
continue;
}
for (BeanId beanId : bean.getReference(name)) {
allRefs.add(beanId);
}
}
Collection<BeanId> missingReferences = new ArrayList<>();
for (BeanId beanId : allRefs) {
if (beanId.getInstanceId() == null) {
continue;
}
if (mapDB.get(beanId) == null && inflightBeans.get(beanId) == null) {
missingReferences.add(beanId);
}
}
if (missingReferences.size() > 0) {
throw CFG301_MISSING_RUNTIME_REF(bean.getId(), missingReferences);
}
}
private void checkUniquness(Bean bean) {
Bean found = mapDB.get(bean.getId());
if (found != null) {
throw CFG303_BEAN_ALREADY_EXIST(bean.getId());
}
}
/**
* Returns the a list of property names of the target bean that have
* references to the bean id.
*/
private static boolean hasReferences(Bean target, BeanId reference) {
for (String name : target.getReferenceNames()) {
List<BeanId> refs = target.getReference(name);
if (refs != null) {
for (BeanId ref : target.getReference(name)) {
if (ref.equals(reference)) {
return true;
}
}
}
}
return false;
}
}