/**
* 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.core.yaml;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.io.Files;
import org.deephacks.confit.admin.query.BeanQuery;
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.PropertyManager;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.deephacks.confit.model.Events.*;
public class YamlBeanManager extends BeanManager {
static final String YAML_BEAN_FILE_STORAGE_DIR_PROP = "config.spi.bean.yaml.dir";
static final String YAML_BEAN_FILE_NAME = "bean.yaml";
static final String YAML_EMPTY_FILE = "{}";
static File file;
private static Yaml yaml = new Yaml();
public YamlBeanManager() {
PropertyManager propertyManager = PropertyManager.lookup();
propertyManager.get(YAML_BEAN_FILE_STORAGE_DIR_PROP);
String dirValue = propertyManager.get(YAML_BEAN_FILE_STORAGE_DIR_PROP).or(System.getProperty("java.io.tmpdir"));
file = new File(new File(dirValue), YAML_BEAN_FILE_NAME);
}
@Override
public Optional<Bean> getEager(BeanId id) {
Map<BeanId, Bean> all = readValuesAsMap();
return getEagerly(id, all);
}
private Optional<Bean> getEagerly(BeanId id, Map<BeanId, Bean> all) {
Bean result = all.get(id);
if (result == null) {
return Optional.absent();
}
// bean found, initalize references.
for (BeanId ref : result.getReferences()) {
if (ref.getBean() != null) {
continue;
}
Bean refBean = all.get(ref);
if (refBean == null) {
throw CFG301_MISSING_RUNTIME_REF(result.getId(), ref);
}
ref.setBean(refBean);
getEagerly(ref, all);
}
return Optional.of(result);
}
@Override
public Optional<Bean> getLazy(BeanId id) throws AbortRuntimeException {
Map<BeanId, Bean> all = readValuesAsMap();
Bean bean = all.get(id);
if (bean == null) {
return Optional.absent();
}
for (BeanId ref : bean.getReferences()) {
Bean refBean = all.get(ref);
if (refBean == null) {
throw CFG301_MISSING_RUNTIME_REF(ref);
}
ref.setBean(refBean);
}
return Optional.of(bean);
}
/**
* The direct, but no further, successors that references this bean will also be
* fetched and initalized with their direct, but no further, predecessors.
*/
@Override
public Map<BeanId, Bean> getBeanToValidate(Collection<Bean> beans) throws AbortRuntimeException {
Map<BeanId, Bean> beansToValidate = new HashMap<>();
for (Bean bean : beans) {
Map<BeanId, Bean> predecessors = new HashMap<>();
// beans read from xml storage will only have their basic properties initalized...
Map<BeanId, Bean> all = readValuesAsMap();
// ... but we also need set the direct references/predecessors for beans to validate
Map<BeanId, Bean> beansToValidateSubset = getDirectSuccessors(bean, all);
beansToValidateSubset.put(bean.getId(), bean);
for (Bean toValidate : beansToValidateSubset.values()) {
predecessors.putAll(getDirectPredecessors(toValidate, all));
}
for (Bean predecessor : predecessors.values()) {
for (BeanId ref : predecessor.getReferences()) {
Bean b = all.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);
}
return beansToValidate;
}
private Map<BeanId, Bean> getDirectPredecessors(Bean bean, Map<BeanId, Bean> all) {
Map<BeanId, Bean> predecessors = new HashMap<>();
for (BeanId ref : bean.getReferences()) {
Bean predecessor = all.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> all) {
Map<BeanId, Bean> successors = new HashMap<>();
for (Bean b : all.values()) {
List<BeanId> refs = b.getReferences();
if (refs.contains(bean.getId())) {
successors.put(b.getId(), b);
}
}
return successors;
}
@Override
public Optional<Bean> getSingleton(String schemaName) throws IllegalArgumentException {
Map<BeanId, Bean> all = readValuesAsMap();
for (Bean bean : all.values()) {
if (bean.getId().getSchemaName().equals(schemaName)) {
if (!bean.getId().isSingleton()) {
throw new IllegalArgumentException("Schema [" + schemaName
+ "] is not a lookup.");
}
BeanId singletonId = bean.getId();
return getEagerly(singletonId, all);
}
}
return Optional.of(Bean.create(BeanId.createSingleton(schemaName)));
}
@Override
public Map<BeanId, Bean> list(String name) {
Map<BeanId, Bean> all = readValuesAsMap();
Map<BeanId, Bean> result = new HashMap<>();
for (Bean b : all.values()) {
if (b.getId().getSchemaName().equals(name)) {
Optional<Bean> bean = getEagerly(b.getId(), all);
if (bean.isPresent()) {
result.put(bean.get().getId(), bean.get());
}
}
}
return result;
}
@Override
public Map<BeanId, Bean> list(String schemaName, Collection<String> ids)
throws AbortRuntimeException {
List<Bean> beans = readValues();
Map<BeanId, Bean> result = new HashMap<>();
for (Bean bean : beans) {
String schema = bean.getId().getSchemaName();
if (!schema.equals(schemaName)) {
continue;
}
for (String id : ids) {
if (bean.getId().getInstanceId().equals(id)) {
result.put(bean.getId(), bean);
}
}
}
return result;
}
@Override
public void create(Bean bean) {
Map<BeanId, Bean> values = readValuesAsMap();
checkReferencesExist(bean, values);
checkUniquness(bean, values);
values.put(bean.getId(), bean);
writeValues(values);
}
@Override
public void create(Collection <Bean> set) {
Map<BeanId, Bean> beans = readValuesAsMap();
// first check uniquness towards storage
for (Bean bean : set) {
checkUniquness(bean, beans);
}
// TODO: check that provided beans are unique among themselves.
// references may not exist in storage, but are provided
// as part of the transactions, so add them before validating references.
for (Bean bean : set) {
beans.put(bean.getId(), bean);
}
for (Bean bean : set) {
checkReferencesExist(bean, beans);
}
writeValues(beans);
}
@Override
public void createSingleton(BeanId singleton) {
Map<BeanId, Bean> values = readValuesAsMap();
Bean bean = Bean.create(singleton);
try {
checkUniquness(bean, values);
} catch (AbortRuntimeException e) {
// ignore and return silently.
return;
}
values.put(singleton, bean);
writeValues(values);
}
@Override
public void set(Bean bean) {
Map<BeanId, Bean> values = readValuesAsMap();
Bean existing = values.get(bean.getId());
if (existing == null) {
throw CFG304_BEAN_DOESNT_EXIST(bean.getId());
}
checkReferencesExist(bean, values);
checkInstanceExist(bean, values);
values.put(bean.getId(), bean);
writeValues(values);
}
@Override
public void set(Collection<Bean> set) {
Map<BeanId, Bean> beans = readValuesAsMap();
// TODO: check that provided beans are unique among themselves.
// references may not exist in storage, but are provided
// as part of the transactions, so add them before validating references.
for (Bean bean : set) {
Bean existing = beans.get(bean.getId());
if (existing == null) {
throw CFG304_BEAN_DOESNT_EXIST(bean.getId());
}
beans.put(bean.getId(), bean);
}
for (Bean bean : set) {
checkReferencesExist(bean, beans);
}
writeValues(beans);
}
@Override
public void merge(Bean bean) {
Map<BeanId, Bean> beans = readValuesAsMap();
Bean b = beans.get(bean.getId());
if (b == null) {
throw CFG304_BEAN_DOESNT_EXIST(bean.getId());
}
replace(b, bean, beans);
writeValues(beans);
}
@Override
public void merge(Collection<Bean> bean) {
Map<BeanId, Bean> beans = readValuesAsMap();
for (Bean replace : bean) {
Bean target = beans.get(replace.getId());
if (target == null) {
throw Events.CFG304_BEAN_DOESNT_EXIST(replace.getId());
}
replace(target, replace, beans);
}
writeValues(beans);
}
private void replace(Bean target, Bean replace, Map<BeanId, Bean> all) {
if (target == null) {
// bean did not exist in storage, create it.
target = replace;
}
checkReferencesExist(replace, all);
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) {
Map<BeanId, Bean> beans = readValuesAsMap();
checkNoReferencesExist(id, beans);
Bean deleted = beans.remove(id);
writeValues(beans);
return deleted;
}
@Override
public Collection<Bean> delete(String schemaName, Collection<String> instanceIds) {
Map<BeanId, Bean> beans = readValuesAsMap();
ArrayList<Bean> deleted = new ArrayList<>();
for (String instance : instanceIds) {
checkNoReferencesExist(BeanId.create(instance, schemaName), beans);
Bean bean = beans.remove(BeanId.create(instance, schemaName));
deleted.add(bean);
}
writeValues(beans);
return deleted;
}
@Override
public BeanQuery newQuery(Schema schema) {
throw new UnsupportedOperationException("");
}
private static void checkNoReferencesExist(BeanId deleted, Map<BeanId, Bean> storage) {
Collection<BeanId> hasReferences = new ArrayList<>();
for (Bean b : storage.values()) {
if (hasReferences(b, deleted)) {
hasReferences.add(b.getId());
}
}
if (hasReferences.size() > 0) {
throw CFG302_CANNOT_DELETE_BEAN(Arrays.asList(deleted));
}
}
private static void checkReferencesExist(final Bean bean, final Map<BeanId, Bean> storage) {
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;
}
Bean b = storage.get(beanId);
if (b == null) {
missingReferences.add(beanId);
}
}
if (missingReferences.size() > 0) {
throw CFG301_MISSING_RUNTIME_REF(bean.getId(), missingReferences);
}
}
private static void checkInstanceExist(Bean bean, Map<BeanId, Bean> storage) {
Collection<Bean> beans = storage.values();
for (Bean existingBean : beans) {
if (existingBean.getId().equals(bean.getId())) {
return;
}
}
throw CFG304_BEAN_DOESNT_EXIST(bean.getId());
}
private static void checkUniquness(Bean bean, Map<BeanId, Bean> storage) {
Collection<Bean> beans = storage.values();
for (Bean existing : beans) {
if (bean.getId().equals(existing.getId())) {
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()) {
for (BeanId ref : target.getReference(name)) {
if (ref.equals(reference)) {
return true;
}
}
}
return false;
}
private List<Bean> readValues() {
ArrayList<Bean> result = new ArrayList<>();
try {
String input = Files.toString(file, Charsets.UTF_8);
Map<Object, Object> map = (Map<Object, Object>) yaml.load(input);
for (Object schemaName : map.keySet()) {
Object rawBeans = map.get(schemaName);
Collection<Object> beans = new ArrayList<>();
if(Collection.class.isAssignableFrom(rawBeans.getClass())) {
beans = (Collection<Object>) rawBeans;
} else {
beans.add(rawBeans);
}
for (Object bean : beans) {
Map<String, Map<String, Object>> beanObject = (Map<String, Map<String, Object>>) bean;
String instanceId = beanObject.keySet().iterator().next();
BeanId id = BeanId.create(instanceId, schemaName.toString());
YamlBean yamlBean = new YamlBean(id, beanObject.get(instanceId));
result.add(yamlBean.toBean());
}
}
} catch (IOException e) {
return new ArrayList<>();
}
return result;
}
private Map<BeanId, Bean> readValuesAsMap() {
List<Bean> beans = readValues();
Map<BeanId, Bean> map = new HashMap<>();
for (Bean bean : beans) {
map.put(bean.getId(), bean);
}
return map;
}
private void writeValues(Map<BeanId, Bean> map) {
writeValues(new ArrayList<Bean>(map.values()));
}
private void writeValues(List<Bean> beans) {
File dir = getStorageDir();
if (!dir.exists()) {
try {
dir.createNewFile();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Multimap<String, YamlBean> transformed = ArrayListMultimap.create();
for (Bean bean : beans) {
transformed.put(bean.getId().getSchemaName(), new YamlBean(bean));
}
Map<String, List<Map<String, Object>>> yamlBeans = new HashMap<>();
for (String schemaName : transformed.keySet()) {
for (YamlBean bean : transformed.get(schemaName)){
Map<String, Object> yaml = bean.toMap();
List<Map<String, Object>> list = yamlBeans.get(schemaName);
if(list == null) {
list = new ArrayList<>();
}
list.add(yaml);
yamlBeans.put(schemaName, list);
}
}
String output = yaml.dumpAsMap(yamlBeans);
File file = new File(dir, YAML_BEAN_FILE_NAME);
try {
Files.write(output.getBytes(), file);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static File getStorageDir() {
return file.getParentFile();
}
}