ReferenceConfigSupplier.java

/*
 * Copyright 2016-2017 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.config.impl;

import net.morimekta.providence.PMessage;
import net.morimekta.providence.PType;
import net.morimekta.providence.config.ConfigListener;
import net.morimekta.providence.config.ConfigSupplier;
import net.morimekta.providence.config.parser.ConfigException;
import net.morimekta.providence.config.util.UncheckedConfigException;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PMessageDescriptor;

import javax.annotation.Nonnull;
import java.time.Clock;
import java.util.Arrays;
import java.util.stream.Collectors;

/**
 * A supplier to get a config (aka message) from a resource location. This is
 * a fixed static supplier, so listening to changes will never do anything.
 *
 * <pre>
 *     ConfigSupplier&lt;Service, Service._Field&gt; supplier =
 *             new SerializedConfigSupplier&lt;&gt;(referencePath, Service.kDescriptor);
 * </pre>
 */
public class ReferenceConfigSupplier<
        RefMessage extends PMessage<RefMessage>,
        ParentMessage extends PMessage<ParentMessage>>
        extends UpdatingConfigSupplier<RefMessage>
        implements ConfigListener<ParentMessage> {
    private final String                        referencePath;
    private final ConfigSupplier<ParentMessage> parent;
    private final PField<?>[] fieldRefs;

    /**
     * Create a config that wraps a providence message instance, and fetches a message from
     * within that parent config. It is not allowed to have it return a null, meaning for
     * the reference config to be valid, the reference must exist.
     *
     * @param parent The message type descriptor.
     * @param clock The clock to use for timing.
     * @param fieldRefs Fields to reference.
     * @throws ConfigException If message overriding failed
     */
    public ReferenceConfigSupplier(ConfigSupplier<ParentMessage> parent, Clock clock, PField<?>... fieldRefs)
            throws ConfigException {
        super(clock);
        this.parent = parent;
        this.fieldRefs = validate(parent.get().descriptor(), fieldRefs);
        this.referencePath = Arrays.stream(fieldRefs).map(PField::getName).collect(Collectors.joining("."));
        parent.addListener(this);
        set(getReference(parent.get(), this.fieldRefs));
    }

    @Override
    public void onConfigChange(@Nonnull ParentMessage updated) {
        try {
            set(getReference(updated, fieldRefs));
        } catch (ConfigException e) {
            throw new UncheckedConfigException(e);
        }
    }

    @Override
    public String toString() {
        return "ReferenceConfig{" + referencePath + ", parent=" + parent.getName() + "}";
    }

    @Override
    public String getName() {
        return "ReferenceConfig{" + referencePath + "}";
    }

    @SuppressWarnings("unchecked")
    static <RM extends PMessage<RM>> RM getReference(PMessage<?> instance, PField<?>... fields)
            throws ConfigException {
        PMessage<?> current = instance;
        for (PField<?> field : fields) {
            if (!current.has(field.getId())) {
                if (!field.hasDefaultValue()) {
                    throw new ConfigException(
                            "Field %s in %s is missing and has no default, from %s",
                            field.getName(), current.descriptor().getQualifiedName(), referencePath(fields));
                }
                current = (PMessage<?>) field.getDefaultValue();
            } else {
                current = current.get(field.getId());
            }
        }
        return (RM) current;
    }

    static PField<?>[] validate(PMessageDescriptor<?> descriptor, PField<?>... fields) throws ConfigException {
        PMessageDescriptor<?> current = descriptor;
        for (PField<?> field : fields) {
            if (!field.equals(current.findFieldByName(field.getName()))) {
                throw new ConfigException(
                        "Bad field %s in %s from %s",
                        field.getName(), current.getQualifiedName(), referencePath(fields));
            }
            if (field.getType() != PType.MESSAGE) {
                throw new ConfigException(
                        "Field %s in %s is not a message, from %s",
                        field.getName(), current.getQualifiedName(), referencePath(fields));
            }
            current = (PMessageDescriptor<?>) field.getDescriptor();
        }
        return fields;
    }

    private static String referencePath(PField<?>... fields) {
        return Arrays.stream(fields).map(PField::getName).collect(Collectors.joining("."));
    }
}