/**
* 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.model;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.deephacks.confit.model.Schema.SchemaProperty;
import org.deephacks.confit.model.Schema.SchemaPropertyList;
import org.deephacks.confit.model.Schema.SchemaPropertyRef;
import org.deephacks.confit.model.Schema.SchemaPropertyRefList;
import org.deephacks.confit.model.Schema.SchemaPropertyRefMap;
import org.deephacks.confit.serialization.Conversion;
import org.deephacks.confit.serialization.UniqueIds;
import org.deephacks.confit.serialization.ValueSerialization.ValueReader;
import org.deephacks.confit.serialization.ValueSerialization.ValueWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import static com.google.common.base.Objects.equal;
import static org.deephacks.confit.model.Events.CFG310_CIRCULAR_REF;
/**
* <p>
* Bean represent an instance of a configurable bean. Every bean
* that are to be provisioned must first have its corresponding schema
* registered in the system.
* </p>
* <p>
* Bean only serve as a simple transfer object and should not be
* concerned about schema specific thing. It should not try to
* understand or validate if specific values are correct.
* </p>
* <p>
* Bean only know about two kinds of values, properties and references. All
* values are treated as a list of plain strings and do not care if certain
* properties in reality are single valued.
* </p>
* @author Kristoffer Sjogren
*/
public final class Bean {
private static final Conversion conversion = Conversion.get();
/** lazy, only needed for binary serialization */
private static UniqueIds ids;
private final BeanId id;
private final HashMap<String, List<String>> properties = new HashMap<>();
private final HashMap<String, List<BeanId>> references = new HashMap<>();
private Bean(BeanId id) {
Preconditions.checkNotNull(id);
// make a defensive copy
if (id.isSingleton()) {
this.id = BeanId.createSingleton(id.getSchemaName());
} else {
this.id = BeanId.create(id.getInstanceId(), id.getSchemaName());
}
}
/**
* Create a admin bean instance.
*
* @param id unique identification of the bean instance.
* @return AdminBean
*/
public static Bean create(final BeanId id) {
Preconditions.checkNotNull(id);
Bean bean = new Bean(id);
bean.set(id.getSchema());
return bean;
}
/**
* @return unique identification of the bean instance.
*/
public BeanId getId() {
return id;
}
/**
* Schema will only be present if the Bean was fetched or given from
* a managed administrative context.
*
* @return the schema that belong to the bean instance.
*/
public Schema getSchema() {
return id.getSchema();
}
/**
* Set the schema that define content structure and constraints of this
* the bean instance.
*
* @param schema schema
*/
public void set(final Schema schema) {
this.id.set(schema);
}
/**
* Return the list of property names which have values. Properties
* with default values are not returned.
*
* @return list of property names.
*/
public List<String> getPropertyNames() {
ArrayList<String> names = new ArrayList<>(properties.keySet());
Collections.sort(names);
return names;
}
/**
* Return the list of property names which are references.
*
* @return list of property names.
*/
public List<String> getReferenceNames() {
ArrayList<String> names = new ArrayList<>(references.keySet());
Collections.sort(names);
return names;
}
/**
* Add a list of value to a property on this bean.
*
* @param propertyName name of the property as defined by the bean's schema.
* @param values final String representations of the parameterized type of the collection
* that conforms to its type as defined by the bean's schema.
*/
public void addProperty(final String propertyName, final Collection<String> values) {
Preconditions.checkNotNull(values);
Preconditions.checkNotNull(propertyName);
List<String> list = properties.get(propertyName);
if (list == null) {
properties.put(propertyName, new ArrayList<>(values));
} else {
list.addAll(values);
}
}
/**
* Add a value to a property on this bean.
*
* @param propertyName name of the property as defined by the bean's schema.
* @param value final String representations of the property that conforms to
* its type as defined by the bean's schema.
*/
public void addProperty(final String propertyName, final String value) {
Preconditions.checkNotNull(propertyName);
Preconditions.checkNotNull(value);
List<String> values = properties.get(propertyName);
if (values == null) {
values = new ArrayList<>();
values.add(value);
properties.put(propertyName, values);
} else {
values.add(value);
}
}
/**
* Overwrite the current values with the provided value.
*
* @param propertyName name of the property as defined by the bean's schema.
* @param value final String representations of the property that conforms to
* its type as defined by the bean's schema.
*/
public void setProperty(final String propertyName, final String value) {
Preconditions.checkNotNull(propertyName);
if (value == null) {
properties.put(propertyName, null);
return;
}
List<String> values = new ArrayList<>();
values.add(value);
properties.put(propertyName, values);
}
/**
* Overwrite/replace the current values with the provided values.
*
* @param propertyName name of the property as defined by the bean's schema.
* @param values final String representations of the property that conforms to
* its type as defined by the bean's schema.
*/
public void setProperty(final String propertyName, final List<String> values) {
Preconditions.checkNotNull(propertyName);
if (values == null) {
properties.put(propertyName, null);
return;
}
properties.put(propertyName, values);
}
/**
* Clear the values of a property or reference, setting it to null.
*
* This operation can be useful when removing properties using "merge"
* operation.
*
* @param propertyName of the property as defined by the bean's schema.
*/
public void clear(final String propertyName) {
Preconditions.checkNotNull(propertyName);
if (properties.containsKey(propertyName)) {
properties.put(propertyName, null);
} else if (references.containsKey(propertyName)) {
references.put(propertyName, null);
}
}
/**
* Remove a property or reference as a property from this bean.
*
* This operation can be useful when removing properties using "set"
* operation.
*
* @param propertyName name of the property as defined by the bean's schema.
*/
public void remove(final String propertyName) {
Preconditions.checkNotNull(propertyName);
if (properties.containsKey(propertyName)) {
properties.remove(propertyName);
} else if (references.containsKey(propertyName)) {
references.remove(propertyName);
}
}
/**
* Get the values of a property on a bean.
*
* @param propertyName of the property as defined by the bean's schema.
* @return final String representations of the property that conforms to
* its type as defined by the bean's schema.
*/
public List<String> getValues(final String propertyName) {
Preconditions.checkNotNull(propertyName);
List<String> values = properties.get(propertyName);
if (values == null) {
return null;
}
// creates a shallow defensive copy
return new ArrayList<>(values);
}
/**
* A helper method for getting the value of single valued properties. Returns
* null if the property does not exist.
*
* @param propertyName name of the property as defined by the bean's schema.
* @return final String representations of the property that conforms to
* its type as defined by the bean's schema.
*/
public String getSingleValue(final String propertyName) {
Preconditions.checkNotNull(propertyName);
List<String> values = getValues(propertyName);
if (values == null || values.size() < 1) {
return null;
}
return values.get(0);
}
/**
* Add a list of references to a property on this bean.
*
* A reference identify other beans based on schema and instance id.
*
* @param propertyName name of the property as defined by the bean's schema.
* @param refs the reference as defined by the bean's schema.
*/
public void addReference(final String propertyName, final Collection<BeanId> refs) {
Preconditions.checkNotNull(refs);
Preconditions.checkNotNull(propertyName);
checkCircularReference(refs.toArray(new BeanId[refs.size()]));
List<BeanId> list = references.get(propertyName);
if (list == null) {
list = new ArrayList<>();
list.addAll(refs);
references.put(propertyName, list);
} else {
list.addAll(refs);
}
}
/**
* Check that references does not point to self.
* @param references from
*/
private void checkCircularReference(final BeanId... references) {
for (BeanId beanId : references) {
if (getId().equals(beanId)) {
throw CFG310_CIRCULAR_REF(getId(), getId());
}
}
}
/**
* Add a reference to a property on this bean.
*
* A reference identify other beans based on schema and instance id.
*
*
* @param propertyName name of the property as defined by the bean's schema.
* @param ref the reference as defined by the bean's schema.
*/
public void addReference(final String propertyName, final BeanId ref) {
Preconditions.checkNotNull(ref);
Preconditions.checkNotNull(propertyName);
checkCircularReference(ref);
List<BeanId> list = references.get(propertyName);
if (list == null) {
list = new ArrayList<>();
list.add(ref);
references.put(propertyName, list);
} else {
list.add(ref);
}
}
/**
* Get the references of a property on a bean.
*
* @param propertyName name of the property as defined by the bean's schema.
* @return References that identify other beans.
*/
public List<BeanId> getReference(final String propertyName) {
List<BeanId> values = references.get(propertyName);
if (values == null) {
return null;
}
return values;
}
/**
* Get all references for all properties of this bean.
*
* @return References that identify other beans.
*/
public List<BeanId> getReferences() {
if (references == null) {
return new ArrayList<>();
}
ArrayList<BeanId> result = new ArrayList<>();
for (List<BeanId> b : references.values()) {
if (b != null) {
result.addAll(b);
}
}
return result;
}
/**
* Overwrite/replace the current references with the provided reference.
*
* @param propertyName name of the property as defined by the bean's schema.
* @param values override
*/
public void setReferences(final String propertyName, final List<BeanId> values) {
Preconditions.checkNotNull(propertyName);
if (values == null || values.size() == 0) {
references.put(propertyName, null);
return;
}
checkCircularReference(values.toArray(new BeanId[values.size()]));
references.put(propertyName, values);
}
/**
* Overwrite/replace the current references with a provided reference.
*
* @param propertyName name of the property as defined by the bean's schema.
* @param value override
*/
public void setReference(final String propertyName, final BeanId value) {
Preconditions.checkNotNull(propertyName);
if (value == null) {
references.put(propertyName, null);
return;
}
checkCircularReference(value);
List<BeanId> values = new ArrayList<>();
values.add(value);
references.put(propertyName, values);
}
/**
* A helper method for getting the value of single referenced property. Returns
* null if the refrences does not exist.
*
* @param propertyName name of the property as defined by the bean's schema.
* @return the value.
*/
public BeanId getFirstReference(final String propertyName) {
List<BeanId> refrences = getReference(propertyName);
if (refrences == null || refrences.size() < 1) {
return null;
}
return refrences.get(0);
}
/**
* Clears this bean from all properties and references.
*/
public void clear() {
properties.clear();
references.clear();
}
@Override
public int hashCode() {
return Objects.hashCode(getId());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Bean)) {
return false;
}
Bean other = (Bean) obj;
return equal(getId(), other.getId());
}
@Override
public final String toString() {
return Objects.toStringHelper(Bean.class).add("id", id).add("schema", getSchema())
.add("properties", properties).add("references", references).toString();
}
public static Collection<Bean> copy(Collection<Bean> beans) {
Collection<Bean> copies = new ArrayList<>();
for (Bean bean : beans) {
copies.add(copy(bean));
}
return copies;
}
public static Bean copy(Bean bean) {
if (bean == null) {
return null;
}
Bean copy = Bean.create(bean.getId());
for (String property : bean.getPropertyNames()) {
Collection<String> values = bean.getValues(property);
if (values == null) {
continue;
}
copy.setProperty(property, bean.getValues(property));
}
for (String property : bean.getReferenceNames()) {
List<BeanId> ids = bean.getReference(property);
if (ids == null) {
continue;
}
for (BeanId id : ids) {
copy.addReference(property, BeanId.create(id.getInstanceId(), id.getSchemaName()));
}
}
return copy;
}
public byte[] write() {
if (ids == null) {
ids = UniqueIds.lookup();
}
Schema schema = getSchema();
Preconditions.checkNotNull(schema);
ValueWriter writer = new ValueWriter();
for (SchemaProperty property : schema.get(SchemaProperty.class)) {
int propId = ids.getSchemaId(property.getFieldName());
Object value;
if(writer.isBasicType(property.getClassType())) {
value = conversion.convert(getSingleValue(property.getFieldName()), property.getClassType());
} else {
value = getSingleValue(property.getFieldName());
}
writer.putValue(propId, value);
}
for (SchemaPropertyList property : schema.get(SchemaPropertyList.class)) {
int propId = ids.getSchemaId(property.getFieldName());
Collection<?> values;
if(writer.isBasicType(property.getClassType())) {
values = conversion.convert(getValues(property.getFieldName()), property.getClassType());
writer.putValues(propId, values, property.getClassType());
} else {
values = getValues(property.getFieldName());
writer.putValues(propId, values, String.class);
}
}
for (SchemaPropertyRef property : schema.get(SchemaPropertyRef.class)) {
int propId = ids.getSchemaId(property.getFieldName());
BeanId beanId = getFirstReference(property.getFieldName());
if (beanId != null) {
writer.putValue(propId, beanId.getInstanceId());
}
}
for (SchemaPropertyRefList property : schema.get(SchemaPropertyRefList.class)) {
int propId = ids.getSchemaId(property.getFieldName());
ArrayList<String> list = new ArrayList<>();
List<BeanId> beanIds = getReference(property.getFieldName());
if(beanIds != null) {
for (BeanId id : beanIds) {
list.add(id.getInstanceId());
}
writer.putValues(propId, list, String.class);
}
}
for (SchemaPropertyRefMap property : schema.get(SchemaPropertyRefMap.class)) {
int propId = ids.getSchemaId(property.getFieldName());
ArrayList<String> list = new ArrayList<>();
List<BeanId> beanIds = getReference(property.getFieldName());
if(beanIds != null) {
for (BeanId id : beanIds) {
list.add(id.getInstanceId());
}
writer.putValues(propId, list, String.class);
}
}
return writer.write();
}
public static Bean read(BeanId beanId, byte[] data) {
if (ids == null) {
ids = UniqueIds.lookup();
}
Preconditions.checkNotNull(beanId);
Preconditions.checkNotNull(data);
Schema schema = beanId.getSchema();
Preconditions.checkNotNull(schema);
Bean bean = Bean.create(beanId);
ValueReader reader = new ValueReader(data);
Multimap<String, String> properties = ArrayListMultimap.create();
Multimap<String, BeanId> references = ArrayListMultimap.create();
int[] propertyIds = reader.getIds();
for (int id : propertyIds) {
String propertyName = ids.getSchemaName(id);
Object value = reader.getValue(id);
if (schema.isProperty(propertyName)) {
if (Collection.class.isAssignableFrom(value.getClass())) {
properties.putAll(propertyName, conversion.convert((Collection)value, String.class));
} else {
properties.put(propertyName, conversion.convert(value, String.class));
}
} else if (schema.isReference(propertyName)) {
String schemaName = schema.getReferenceSchemaName(propertyName);
if (Collection.class.isAssignableFrom(value.getClass())) {
for (String instanceId : (Collection<String>) value) {
references.put(propertyName, BeanId.create(instanceId, schemaName));
}
} else {
String instanceId = (String) value;
references.put(propertyName, BeanId.create(instanceId, schemaName));
}
} else {
throw new IllegalArgumentException("Unrecognized property " + propertyName);
}
}
for (String propertyName : properties.keySet()) {
bean.addProperty(propertyName, properties.get(propertyName));
}
for (String propertyName : references.keySet()) {
bean.addReference(propertyName, references.get(propertyName));
}
bean.set(schema);
return bean;
}
}