CUnion.java

/*
 * Copyright 2016 Providence Authors
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 net.morimekta.providence.reflect.contained;

import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageBuilder;
import net.morimekta.providence.PType;
import net.morimekta.providence.PUnion;
import net.morimekta.providence.descriptor.PAnnotation;
import net.morimekta.providence.descriptor.PContainer;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PUnionDescriptor;
import net.morimekta.util.collect.UnmodifiableList;
import net.morimekta.util.collect.UnmodifiableMap;
import net.morimekta.util.collect.UnmodifiableSet;
import net.morimekta.util.collect.UnmodifiableSortedMap;
import net.morimekta.util.collect.UnmodifiableSortedSet;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * @author Stein Eldar Johnsen
 * @since 07.09.15
 */
public class CUnion implements PUnion<CUnion> {
    private final CUnionDescriptor descriptor;
    private final CField<CUnion>   unionField;
    private final Object           unionValue;

    @SuppressWarnings("unchecked")
    private CUnion(Builder builder) {
        this.unionField = builder.unionField;
        this.descriptor = builder.descriptor;

        if (builder.currentValue instanceof PMessageBuilder) {
            this.unionValue = ((PMessageBuilder<?>) builder.currentValue).build();
        } else if (builder.currentValue instanceof List) {
            this.unionValue = UnmodifiableList.copyOf((List<?>) builder.currentValue);
        } else if (builder.currentValue instanceof Map) {
            if (PContainer.typeForName(unionField.getAnnotationValue(PAnnotation.CONTAINER)) ==
                PContainer.Type.SORTED) {
                this.unionValue = UnmodifiableSortedMap.copyOf((Map<?,?>) builder.currentValue);
            } else {
                this.unionValue = UnmodifiableMap.copyOf((Map<?,?>) builder.currentValue);
            }
        } else if (builder.currentValue instanceof Set) {
            if (PContainer.typeForName(unionField.getAnnotationValue(PAnnotation.CONTAINER)) ==
                PContainer.Type.SORTED) {
                this.unionValue = UnmodifiableSortedSet.copyOf((Set<?>) builder.currentValue);
            } else {
                this.unionValue = UnmodifiableSet.copyOf((Set<?>) builder.currentValue);
            }
        } else {
            this.unionValue = builder.currentValue;
        }
    }

    @Override
    public boolean has(int key) {
        return unionField != null && unionField.getId() == key && unionValue != null;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T get(int key) {
        return has(key) ? (T) unionValue : null;
    }

    @Nonnull
    @Override
    public PMessageBuilder<CUnion> mutate() {
        return new Builder(this);
    }

    @Nonnull
    @Override
    public String asString() {
        return CStruct.asString(this);
    }

    @Override
    public String toString() {
        return descriptor.getQualifiedName() + asString();
    }

    @Nonnull
    @Override
    public CUnionDescriptor descriptor() {
        return descriptor;
    }

    @Override
    public boolean unionFieldIsSet() {
        return unionField != null;
    }

    @Nonnull
    @Override
    public CField<CUnion> unionField() {
        if (unionField == null) throw new IllegalStateException("No union field set in " + descriptor.getQualifiedName());
        return unionField;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof CUnion)) {
            return false;
        }

        CUnion other = (CUnion) o;
        return Objects.equals(descriptor, other.descriptor) &&
               Objects.equals(unionField, other.unionField) &&
               Objects.equals(unionValue, other.unionValue);
    }

    @Override
    public int hashCode() {
        return Objects.hash(descriptor().getQualifiedName(), unionField, unionValue);
    }

    @Override
    public int compareTo(@Nonnull CUnion other) {
        return CStruct.compareMessages(this, other);
    }

    public static class Builder extends PMessageBuilder<CUnion> {
        private final CUnionDescriptor descriptor;
        private       CField<CUnion>   originalField;
        private       boolean          updated;

        private CField<CUnion> unionField;
        private Object currentValue;

        public Builder(CUnionDescriptor descriptor) {
            this.descriptor = descriptor;
            this.originalField = null;
            this.updated = false;
        }

        public Builder(CUnion union) {
            this.descriptor = union.descriptor;
            if (union.unionField != null) {
                this.set(union.unionField, union.unionValue);
            }
            this.originalField = union.unionField;
            this.updated = false;
        }

        @Nonnull
        @Override
        public PMessageBuilder<?> mutator(int key) {
            CField<CUnion> field = descriptor.findFieldById(key);
            if (field == null) {
                throw new IllegalArgumentException("No such field ID " + key);
            } else if (field.getType() != PType.MESSAGE) {
                throw new IllegalArgumentException("Not a message field ID " + key + ": " + field.getName());
            }
            if (unionField != field) {
                unionField = field;
                currentValue = null;
            }

            updated = true;
            if (currentValue == null) {
                currentValue = ((PMessageDescriptor<?>) field.getDescriptor()).builder();
            } else if (currentValue instanceof PMessage) {
                currentValue = ((PMessage<?>) currentValue).mutate();
            } else if (!(currentValue instanceof PMessageBuilder)) {
                // This should in theory not be possible. This is just a safe-guard.
                throw new IllegalArgumentException("Invalid currentValue in map on message type: " + currentValue.getClass().getSimpleName());
            }

            return (PMessageBuilder<?>) currentValue;
        }

        @Nonnull
        @Override
        @SuppressWarnings("unchecked,rawtypes")
        public Builder merge(@Nonnull CUnion from) {
            if (unionField == null || unionField != from.unionField) {
                if (from.unionField != null) {
                    set(from.unionField.getId(), from.unionValue);
                }
            } else {
                this.updated = true;
                int key = unionField.getId();
                switch (unionField.getType()) {
                    case MESSAGE: {
                        PMessageBuilder src;
                        if (currentValue instanceof PMessageBuilder) {
                            src = (PMessageBuilder<?>) currentValue;
                        } else {
                            src = ((PMessage<?>) currentValue).mutate();
                        }
                        PMessage toMerge = from.get(key);

                        currentValue = src.merge(toMerge);
                        break;
                    }
                    case SET:
                        ((Set<Object>) currentValue).addAll(from.get(key));
                        break;
                    case MAP:
                        ((Map<Object, Object>) currentValue).putAll(from.get(key));
                        break;
                    default:
                        // Lists replace, not add.
                        set(key, from.get(key));
                        break;
                }
            }

            return this;
        }

        @Override
        public boolean has(int key) {
            CField<?> field = descriptor.findFieldById(key);
            return field != null && unionField == field;
        }

        @Override
        @SuppressWarnings("unchecked")
        public <T> T get(int key) {
            CField<?> field = descriptor.findFieldById(key);
            if (unionField == field) {
                return (T) currentValue;
            }
            return null;
        }

        @Nonnull
        @Override
        public PUnionDescriptor<CUnion> descriptor() {
            return descriptor;
        }

        @Nonnull
        @Override
        public CUnion build() {
            return new CUnion(this);
        }

        @Override
        public boolean valid() {
            return unionField != null && currentValue != null;
        }

        @Override
        public Builder validate() {
            if (!valid()) {
                throw new IllegalStateException("No union field set in " +
                                                descriptor().getQualifiedName());
            }
            return this;
        }

        @Nonnull
        @Override
        public Builder set(int key, Object value) {
            CField<CUnion> field = descriptor.findFieldById(key);
            if (field == null) {
                return this; // soft ignoring unsupported fields.
            }
            if (value == null) {
                return clear(key);
            }
            this.updated = true;
            this.unionField = field;
            switch (field.getType()) {
                case SET:
                    this.currentValue = new LinkedHashSet<>((Collection<?>) value);
                    break;
                case LIST:
                    this.currentValue = new ArrayList<>((Collection<?>) value);
                    break;
                case MAP:
                    this.currentValue = new LinkedHashMap<>((Map<?,?>) value);
                    break;
                default:
                    this.currentValue = value;
                    break;
            }

            return this;
        }

        @Override
        public boolean isSet(int key) {
            return unionField != null && unionField.getId() == key;
        }

        @Override
        public boolean isModified(int key) {
            if (updated) {
                if (unionField != null && unionField.getId() == key) return true;
                return originalField != null && originalField.getId() == key;
            }
            return false;
        }

        @Nonnull
        @Override
        @SuppressWarnings("unchecked")
        public Builder addTo(int key, Object value) {
            CField<CUnion> field = descriptor.findFieldById(key);
            if (field == null) {
                return this; // soft ignoring unsupported fields.
            }
            if (field.getType() != PType.LIST &&
                field.getType() != PType.SET) {
                throw new IllegalArgumentException("Unable to accept addTo on non-list field " + field.getName());
            }
            if (value == null) {
                throw new IllegalArgumentException("Adding null item to collection " + field.getName());
            }
            this.updated = true;
            if (this.unionField != field || this.currentValue == null) {
                this.unionField = field;
                switch (field.getType()) {
                    case LIST: {
                        this.currentValue = new ArrayList<>();
                        break;
                    }
                    case SET: {
                        this.currentValue = new LinkedHashSet<>();
                        break;
                    }
                    default:
                        break;
                }
            }
            ((Collection<Object>) this.currentValue).add(value);
            return this;
        }

        @Nonnull
        @Override
        public Builder clear(int key) {
            if (isSet(key)) {
                this.updated = true;
                if (originalField == null) {
                    originalField = unionField;
                }
                this.unionField = null;
                this.currentValue = null;
            }
            return this;
        }
    }
}