CMessageBuilder.java

/*
 * 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.descriptor.PAnnotation;
import net.morimekta.providence.descriptor.PContainer;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PRequirement;
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.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * Base message builder class for contained messages.
 */
public abstract class CMessageBuilder<Builder extends CMessageBuilder<Builder, Message>,
                                      Message extends PMessage<Message>>
        extends PMessageBuilder<Message> {
    private final Map<Integer, Object> values;
    private final Set<Integer>         modified;

    public CMessageBuilder() {
        this.values = new TreeMap<>();
        this.modified = new TreeSet<>();
    }

    @Nonnull
    @Override
    @SuppressWarnings("unchecked")
    public Builder merge(@Nonnull Message from) {
        for (PField<?> field : descriptor().getFields()) {
            int key = field.getId();
            if (from.has(key)) {
                switch (field.getType()) {
                    case MESSAGE:
                        if (values.containsKey(key)) {
                            mutator(key).merge(from.get(key));
                        } else {
                            set(key, from.get(key));
                        }
                        break;
                    case SET:
                        if (values.containsKey(key)) {
                            Set<Object> set = (Set<Object>) values.get(key);
                            if (!(set instanceof LinkedHashSet)) {
                                set = new LinkedHashSet<>(set);
                                values.put(key, set);
                            }
                            set.addAll(from.get(key));
                        } else {
                            set(key, from.get(key));
                        }
                        break;
                    case MAP:
                        if (values.containsKey(key)) {
                            Map<Object, Object> map = (Map<Object, Object>) values.get(key);
                            if (!(map instanceof LinkedHashMap)) {
                                map = new LinkedHashMap<>(map);
                                values.put(key, map);
                            }
                            map.putAll(from.get(key));
                        } else {
                            set(key, from.get(key));
                        }
                        break;
                    default:
                        set(key, from.get(key));
                        break;
                }
                modified.add(key);
            }
        }

        return (Builder) this;
    }

    @Nonnull
    @Override
    public PMessageBuilder<?> mutator(int key) {
        PField<?> 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());
        }

        Object current = values.get(key);
        if (current == null) {
            current = ((PMessageDescriptor<?>) field.getDescriptor()).builder();
            values.put(key, current);
        } else if (current instanceof PMessage) {
            current = ((PMessage<?>) current).mutate();
            values.put(key, current);
        } else if (!(current instanceof PMessageBuilder)) {
            // This should in theory not be possible. This is just a safe-guard.
            throw new IllegalArgumentException("Invalid value in map on message type: " + current.getClass().getSimpleName());
        }
        modified.add(key);

        return (PMessageBuilder<?>) current;
    }

    @Override
    public boolean valid() {
        for (PField<?> field : descriptor().getFields()) {
            if (field.getRequirement() == PRequirement.REQUIRED) {
                if (!values.containsKey(field.getId())) {
                    return false;
                }
            }
        }

        return true;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Builder validate() {
        ArrayList<String> missing = new ArrayList<>();
        for (PField<?> field : descriptor().getFields()) {
            if (field.getRequirement() == PRequirement.REQUIRED) {
                if (!values.containsKey(field.getId())) {
                    missing.add(field.getName());
                }
            }
        }

        if (missing.size() > 0) {
            throw new IllegalStateException(
                    "Missing required fields " +
                    String.join(",", missing) +
                    " in message " + descriptor().getQualifiedName());
        }
        return (Builder) this;
    }

    @Nonnull
    @Override
    @SuppressWarnings("unchecked")
    public Builder set(int key, Object value) {
        CField<?> field = (CField<?>) descriptor().findFieldById(key);
        if (field == null) {
            return (Builder) this; // soft ignoring unsupported fields.
        }
        if (value == null) {
            values.remove(key);
        } else {
            switch (field.getType()) {
                case LIST: {
                    values.put(key, UnmodifiableList.copyOf((Collection<?>) value));
                    break;
                }
                case SET: {
                    PContainer.Type ctype = PContainer.typeForName(field.getAnnotationValue(PAnnotation.CONTAINER));
                    if (ctype == PContainer.Type.SORTED) {
                        values.put(key, UnmodifiableSortedSet.copyOf((Collection<?>) value));
                    } else {
                        values.put(key, UnmodifiableSet.copyOf((Collection<?>) value));
                    }
                    break;
                }
                case MAP: {
                    PContainer.Type ctype = PContainer.typeForName(field.getAnnotationValue(PAnnotation.CONTAINER));
                    if (ctype == PContainer.Type.SORTED) {
                        values.put(key, UnmodifiableSortedMap.copyOf((Map<?,?>) value));
                    } else {
                        values.put(key, UnmodifiableMap.copyOf((Map<?,?>) value));
                    }
                    break;
                }
                default:
                    values.put(key, value);
                    break;
            }
        }

        modified.add(key);
        return (Builder) this;
    }

    @Override
    public boolean isSet(int key) {
        return values.containsKey(key);
    }

    @Override
    public boolean isModified(int key) {
        return modified.contains(key);
    }

    @Nonnull
    @Override
    @SuppressWarnings("unchecked")
    public Builder addTo(int key, Object value) {
        CField<?> field = (CField<?>) descriptor().findFieldById(key);
        if (field == null) {
            return (Builder) this; // soft ignoring unsupported fields.
        }
        if (value == null) {
            throw new IllegalArgumentException("Adding null value");
        }
        if (field.getType() == PType.LIST) {
            List<Object> list = (List<Object>) values.get(field.getId());
            if (list == null) {
                list = new ArrayList<>();
                values.put(field.getId(), list);
            } else if (!(list instanceof ArrayList)) {
                list = new ArrayList<>(list);
                values.put(field.getId(), list);
            }
            list.add(value);
        } else if (field.getType() == PType.SET) {
            Set<Object> set = (Set<Object>) values.get(field.getId());
            if (set == null) {
                set = new LinkedHashSet<>();
                values.put(field.getId(), set);
            } else if (!(set instanceof LinkedHashSet)) {
                set = new LinkedHashSet<>(set);
                values.put(field.getId(), set);
            }
            set.add(value);
        } else {
            throw new IllegalArgumentException("Field " + field.getName() + " in " + descriptor().getQualifiedName() + " is not a collection: " + field.getType());
        }
        modified.add(key);
        return (Builder) this;
    }

    @Nonnull
    @Override
    @SuppressWarnings("unchecked")
    public Builder clear(int key) {
        values.remove(key);
        modified.add(key);
        return (Builder) this;
    }

    @SuppressWarnings("unchecked")
    Map<Integer, Object> getValueMap() {
        UnmodifiableMap.Builder<Integer, Object> out = UnmodifiableMap.builder();
        for (CField<?> field : (CField<?>[]) descriptor().getFields()) {
            int key = field.getId();
            if (values.containsKey(key)) {
                switch (field.getType()) {
                    case LIST:
                        out.put(key, UnmodifiableList.copyOf((List<Object>) values.get(key)));
                        break;
                    case SET: {
                        PContainer.Type ctype = PContainer.typeForName(field.getAnnotationValue(PAnnotation.CONTAINER));
                        switch (ctype) {
                            case SORTED:
                                out.put(key, UnmodifiableSortedSet.copyOf((Set<?>) values.get(key)));
                                break;
                            default:
                                out.put(key, UnmodifiableSet.copyOf((Set<?>) values.get(key)));
                                break;
                        }
                        break;
                    }
                    case MAP: {
                        PContainer.Type ctype = PContainer.typeForName(field.getAnnotationValue(PAnnotation.CONTAINER));
                        switch (ctype) {
                            case SORTED:
                                out.put(key, UnmodifiableSortedMap.copyOf((Map<?,?>) values.get(key)));
                                break;
                            default:
                                out.put(key, UnmodifiableMap.copyOf((Map<?,?>) values.get(key)));
                                break;
                        }
                        break;
                    }
                    case MESSAGE:
                        Object current = values.get(key);
                        if (current instanceof PMessageBuilder) {
                            out.put(key, ((PMessageBuilder<?>) current).build());
                        } else {
                            out.put(key, current);
                        }
                        break;
                    default:
                        out.put(key, values.get(key));
                        break;
                }
            } else if (field.getRequirement() != PRequirement.OPTIONAL) {
                // Should always be set. Meaning has() always has a value,
                // if one can be obtained.
                if (field.hasDefaultValue()) {
                    out.put(key, field.getDefaultValue());
                } else if (field.getDescriptor().getDefaultValue() != null) {
                    out.put(key, field.getDescriptor().getDefaultValue());
                }
            }
        }
        return out.build();
    }

    @Override
    public String toString() {
        return descriptor().getQualifiedName() +
               "._Builder{values=" + values +
               ", modified=" + modified + "}";
    }

    @Override
    public boolean has(int key) {
        CField<?> field = (CField<?>) descriptor().findFieldById(key);
        if (field == null) return false;
        if (field.getRequirement() != PRequirement.OPTIONAL &&
            field.getDefaultValue() != null) {
            return true;
        }
        return values.containsKey(key);
    }

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