com.blackducksoftware.bdio.model.AbstractModel.java Source code

Java tutorial

Introduction

Here is the source code for com.blackducksoftware.bdio.model.AbstractModel.java

Source

/*
 * Copyright 2015 Black Duck Software, Inc.
 *
 * 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.blackducksoftware.bdio.model;

import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import javax.annotation.Nullable;

import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.blackducksoftware.bdio.Node;
import com.blackducksoftware.bdio.Term;
import com.blackducksoftware.bdio.Type;
import com.blackducksoftware.bdio.io.LinkedDataContext;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
 * Base class for the models.
 *
 * @author jgustie
 */
public abstract class AbstractModel<M extends AbstractModel<M>> implements Node {
    /**
     * A function that converts to a specific model implementation.
     */
    private static final class ToModelFunction<M extends AbstractModel<? super M>>
            implements Function<Node, Iterable<M>> {
        private final Logger logger = LoggerFactory.getLogger(getClass());

        private final Class<M> modelType;

        private ToModelFunction(Class<M> modelType) {
            this.modelType = checkNotNull(modelType);
        }

        @Override
        public Iterable<M> apply(@Nullable Node node) {
            if (node != null) {
                try {
                    M model = modelType.getConstructor().newInstance();
                    if (node.types().containsAll(model.types())) {
                        model.setId(node.id());
                        model.data().putAll(node.data());
                        return ImmutableSet.of(model);
                    }
                } catch (ReflectiveOperationException e) {
                    logger.debug("Failed to create instance of {}", modelType.getName(), e);
                }
            }
            return ImmutableSet.of();
        }
    }

    /**
     * Abstraction over manipulating a bean field from a {@code Map}. Primarily exists so we don't need to use
     * reflection which can be error prone in these mapping/conversion scenarios.
     */
    protected abstract static class ModelField<M extends AbstractModel<M>, T> {
        /**
         * The term the field maps to.
         */
        private final Term term;

        protected ModelField(Term term) {
            this.term = checkNotNull(term);
        }

        /**
         * Returns the current value of this field from the model.
         * <p>
         * The default implementation returns {@code null}.
         */
        @Nullable
        protected T get(M model) {
            checkNotNull(model);
            return null;
        }

        /**
         * Sets the new value of this field on the model. May perform validation or conversion as necessary.
         * <p>
         * The default implementation throws an unsupported operation exception.
         */
        protected void set(M model, @Nullable Object value) {
            checkNotNull(model);
            throw new UnsupportedOperationException();
        }

        /**
         * Removes this field from the model.
         * <p>
         * The default implementation sets the value to {@code null}.
         */
        protected void remove(M model) {
            set(checkNotNull(model), null);
        }
    }

    /**
     * A live map backed by the enclosing model.
     */
    private static final class ModelMap<M extends AbstractModel<M>> extends AbstractMap<Term, Object> {
        private final M model;

        private final Map<Term, ModelField<M, ?>> fields;

        private final Map<Term, Object> extraData = new LinkedHashMap<>();

        private ModelMap(M model, Iterable<ModelField<M, ?>> fields) {
            this.model = model;
            this.fields = Maps.uniqueIndex(fields, new Function<ModelField<?, ?>, Term>() {
                @Override
                public Term apply(ModelField<?, ?> field) {
                    return field.term;
                }
            });
        }

        @Override
        public Set<Term> keySet() {
            return Sets.union(extraData.keySet(), fields.keySet());
        }

        @Override
        public boolean containsKey(Object key) {
            return fields.containsKey(key) || extraData.containsKey(key);
        }

        @Override
        public int size() {
            return fields.size() + extraData.size();
        }

        @Override
        public Set<Entry<Term, Object>> entrySet() {
            return Maps.asMap(keySet(), Functions.forMap(this)).entrySet();
        }

        @Override
        public Object get(Object key) {
            ModelField<M, ?> field = fields.get(key);
            return field != null ? field.get(model) : extraData.get(key);
        }

        @Override
        public Object remove(Object key) {
            ModelField<M, ?> field = fields.get(key);
            if (field != null) {
                Object originalValue = field.get(model);
                field.remove(model);
                return originalValue;
            } else {
                return extraData.remove(key);
            }
        }

        @Override
        public Object put(Term key, Object value) {
            ModelField<M, ?> field = fields.get(key);
            if (field != null) {
                Object originalValue = field.get(model);
                field.set(model, value);
                return originalValue;
            } else {
                return extraData.put(key, value);
            }
        }
    }

    /**
     * A function that converts an object to a string using the {@code valueToString} function.
     */
    private static Function<Object, String> VALUE_TO_STRING = new Function<Object, String>() {
        @Override
        public String apply(Object value) {
            return valueToString(value);
        }
    };

    /**
     * A function that converts an object to a node using the {@code valueToNode} function.
     */
    private static Function<Object, Node> VALUE_TO_NODE = new Function<Object, Node>() {
        @Override
        public Node apply(Object value) {
            return valueToNode(value);
        }
    };

    /**
     * The current identifier for this model.
     */
    private String id;

    /**
     * The type of this model. Cannot change, stored as an immutable set to match the expected return type.
     */
    private final Set<Type> types;

    /**
     * A live view of this model that delegates to the {@link #lookup(Term)} and {@link #store(Term, Object)} methods.
     */
    private final Map<Term, Object> data;

    AbstractModel(Type type, Iterable<ModelField<M, ?>> fields) {
        types = ImmutableSet.of(type);
        data = Maps.filterValues(new ModelMap<>(self(), fields), Predicates.notNull());
    }

    @SuppressWarnings("unchecked")
    protected final M self() {
        // This should be safe right? Because AbstractModel<M extends AbstractModel<M>>...
        return (M) this;
    }

    /**
     * Alternate to clone or copy constructor, both of which are not feasible here. Resets the state of this object to
     * match the other object and returns this object.
     */
    public M copyOf(M other) {
        this.setId(other.getId());
        this.data().clear();
        this.data().putAll(other.data());
        return self();
    }

    /**
     * Proper accessor for the identifier.
     */
    public final String getId() {
        return id;
    }

    /**
     * Proper mutator for the identifier.
     */
    public final void setId(String id) {
        this.id = id;
    }

    @Override
    public final String id() {
        return id;
    }

    @Override
    public final Set<Type> types() {
        return types;
    }

    // THIS MAP IS NOT NULL-SAFE! No values will come back null, no values can go in null
    @Override
    public final Map<Term, Object> data() {
        return data;
    }

    // Stay consistent with other node implementations

    @Override
    public final int hashCode() {
        return Objects.hash(id, types, data);
    }

    @Override
    public final boolean equals(@Nullable Object obj) {
        if (obj instanceof Node) {
            Node other = (Node) obj;
            return Objects.equals(id, other.id()) && Objects.equals(types, other.types())
                    && Objects.equals(data, other.data());
        } else {
            return false;
        }
    }

    @Override
    public final String toString() {
        return toStringHelper(this).add("id", id).add("types", types).add("data", data).toString();
    }

    // Helpers to implement list methods

    /**
     * Helper to safely add a value to a list. If the current value is {@code null}, a new array list is created.
     */
    protected <T, C extends List<? super T>> M safeAddArrayList(ModelField<M, C> field, T value) {
        final M model = self();
        if (value != null) {
            C currentValue = field.get(model);
            if (currentValue != null) {
                try {
                    currentValue.add(value);
                } catch (UnsupportedOperationException e) {
                    List<Object> newValue = new ArrayList<>(currentValue.size() + 1);
                    newValue.addAll(currentValue);
                    newValue.add(value);
                    field.set(model, newValue);
                }
            } else {
                List<Object> newValue = new ArrayList<>();
                newValue.add(value);
                field.set(model, newValue);
            }
        }
        return model;
    }

    /**
     * Helper to safely stream an iterable. If the current value is {@code null}, an empty stream is returned.
     */
    protected <T> FluentIterable<T> safeGet(ModelField<M, ? extends Iterable<T>> field) {
        Iterable<T> value = field.get(self());
        if (value != null) {
            return FluentIterable.from(value);
        } else {
            return FluentIterable.from(ImmutableList.<T>of());
        }
    }

    // Helpers to implement the store method

    /**
     * Helper to coerce a value into a string.
     */
    @Nullable
    protected static String valueToString(@Nullable Object value) {
        if (value instanceof Node) {
            return ((Node) value).id();
        } else if (value != null) {
            return value.toString();
        } else {
            return null;
        }
    }

    /**
     * Helper to coerce a value into a boolean.
     */
    @Nullable
    protected static Boolean valueToBoolean(@Nullable Object value) {
        if (value instanceof Boolean) {
            return (Boolean) value;
        } else if (value != null) {
            return Boolean.valueOf(valueToString(value));
        } else {
            return null;
        }
    }

    /**
     * Helper to coerce a value into an integer.
     */
    @Nullable
    protected static Integer valueToInteger(@Nullable Object value) {
        if (value instanceof Number) {
            return ((Number) value).intValue();
        } else if (value != null) {
            return Integer.valueOf(valueToString(value));
        } else {
            return null;
        }
    }

    /**
     * Helper to coerce a value into a long.
     */
    @Nullable
    protected static Long valueToLong(@Nullable Object value) {
        if (value instanceof Number) {
            return ((Number) value).longValue();
        } else if (value != null) {
            return Long.valueOf(valueToString(value));
        } else {
            return null;
        }
    }

    /**
     * Helper to coerce a value into a timestamp.
     */
    @Nullable
    protected static DateTime valueToDateTime(@Nullable Object value) {
        String stringValue = valueToString(value);
        return stringValue != null ? DateTime.parse(stringValue) : null;
    }

    /**
     * Helper to coerce a value into a node.
     */
    @Nullable
    protected static Node valueToNode(@Nullable Object value) {
        if (value instanceof Node) {
            return (Node) value;
        } else if (value instanceof Map<?, ?>) {
            // TODO We probably should not create a linked data context here
            return new LinkedDataContext().expandToNode((Map<?, ?>) value);
        } else {
            return null;
        }
    }

    /**
     * Helper to coerce a value into a sequence of values.
     */
    protected static FluentIterable<String> valueToStrings(@Nullable Object value) {
        if (value instanceof Iterable<?>) {
            return FluentIterable.from((Iterable<?>) value).transform(VALUE_TO_STRING);
        } else if (value != null) {
            return FluentIterable.from(ImmutableSet.of(value)).transform(VALUE_TO_STRING);
        } else {
            return FluentIterable.from(ImmutableSet.<String>of());
        }
    }

    /**
     * Helper to coerce a value into a sequence of nodes.
     */
    protected static FluentIterable<Node> valueToNodes(@Nullable Object value) {
        if (value instanceof Iterable<?>) {
            return FluentIterable.from((Iterable<?>) value).transform(VALUE_TO_NODE);
        } else if (value != null) {
            return FluentIterable.from(ImmutableSet.of(value)).transform(VALUE_TO_NODE);
        } else {
            return FluentIterable.from(ImmutableSet.<Node>of());
        }
    }

    /**
     * Reduce an empty iterable to a {@code null} value. This is useful when using methods like {@link #valueToStrings}
     * or {@link #valueToNodes} where the final value should be {@code null} instead of empty.
     */
    @Nullable
    protected static <T, C extends Iterable<? super T>> C emptyToNull(C iterable) {
        return Iterables.isEmpty(iterable) ? null : iterable;
    }

    /**
     * A function that converts a node to a compatible model node.
     */
    public static <M extends AbstractModel<? super M>> Function<Node, Iterable<M>> toModel(Class<M> modelType) {
        return new ToModelFunction<>(modelType);
    }
}