com.netflix.spinnaker.halyard.config.model.v1.node.Node.java Source code

Java tutorial

Introduction

Here is the source code for com.netflix.spinnaker.halyard.config.model.v1.node.Node.java

Source

/*
 * Copyright 2016 Google, 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.netflix.spinnaker.halyard.config.model.v1.node;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spinnaker.halyard.config.problem.v1.ConfigProblemSetBuilder;
import com.netflix.spinnaker.halyard.core.GlobalApplicationOptions;
import com.netflix.spinnaker.halyard.core.error.v1.HalException;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.zip.CRC32;

import static com.netflix.spinnaker.halyard.config.model.v1.node.NodeDiff.ChangeType.*;
import static com.netflix.spinnaker.halyard.core.problem.v1.Problem.Severity.FATAL;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

/**
 * The "Node" class represents a YAML node in our config hierarchy that can be validated.
 *
 * The motivation for this is to allow us to navigate YAML paths in our halconfig, and validate each node (if necessary)
 * along the way.
 */
@Slf4j
abstract public class Node implements Validatable {

    @JsonIgnore
    public abstract String getNodeName();

    @JsonIgnore
    public String getNameToRoot() {
        return getNameToClass(Halconfig.class);
    }

    @JsonIgnore
    public String getNameToClass(Class<?> clazz) {
        String name = getNodeName();
        if (parent == null || clazz.isAssignableFrom(parent.getClass())) {
            return name;
        } else {
            return parent.getNameToRoot() + "." + name;
        }
    }

    @Override
    public void accept(ConfigProblemSetBuilder psBuilder, Validator v) {
        v.validate(psBuilder, this);
    }

    @JsonIgnore
    public NodeIterator getChildren() {
        return NodeIteratorFactory.makeReflectiveIterator(this);
    }

    /**
     * Checks if the filter matches this node alone.
     *
     * @param filter the filter being checked.
     * @return true iff the filter accepts this node.
     */
    @JsonIgnore
    private boolean matchesLocally(NodeFilter filter) {
        return filter.matches(this);
    }

    /**
     * Checks if the filter matches this node all the way to the root.
     *
     * @param filter the filter being checked.
     * @return true iff the filter accepts this node, as a part of its full context (yaml tree ending at this node).
     */
    @JsonIgnore
    public boolean matchesToRoot(NodeFilter filter) {
        boolean result = matchesLocally(filter);

        if (parent == null || !result) {
            return result;
        }

        return parent.matchesToRoot(filter);
    }

    @JsonIgnore
    public List<String> fieldOptions(ConfigProblemSetBuilder problemSetBuilder, String fieldName) {
        if (fieldName == null || fieldName.isEmpty()) {
            throw new IllegalArgumentException("Input fieldName may not be empty");
        }

        log.info("Looking for options for field " + fieldName + " in node " + getNodeName() + " for type "
                + getClass().getSimpleName());
        String fieldOptions = fieldName + "Options";
        Method optionsMethod = null;

        try {
            optionsMethod = this.getClass().getDeclaredMethod(fieldOptions, ConfigProblemSetBuilder.class);
            optionsMethod.setAccessible(true);

            return (List<String>) optionsMethod.invoke(this, problemSetBuilder);
        } catch (IllegalAccessException | InvocationTargetException e) {
            log.warn("Failed to call " + fieldOptions + "() on " + this.getClass().getSimpleName());

            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            // It's expected that many fields won't supply options endpoints.
            return new ArrayList<>();
        } finally {
            if (optionsMethod != null) {
                optionsMethod.setAccessible(false);
            }
        }
    }

    @JsonIgnore
    public <T> T parentOfType(Class<? extends T> type) {
        Node parent = this.getParent();
        while (parent != null && !type.isAssignableFrom(parent.getClass())) {
            parent = parent.getParent();
        }

        if (parent == null) {
            return null;
        } else {
            return (T) parent;
        }
    }

    @Getter
    @JsonIgnore
    protected Node parent = null;

    @JsonIgnore
    public void parentify() {
        NodeIterator children = getChildren();

        Node child = children.getNext();
        while (child != null) {
            child.parent = this;
            child.parentify();
            child = children.getNext();
        }
    }

    public List<Field> localFiles() {
        Class<?> clazz = getClass();
        List<Field> res = new ArrayList<>();

        while (clazz != null) {
            res.addAll(Arrays.stream(clazz.getDeclaredFields())
                    .filter(f -> f.getDeclaredAnnotation(LocalFile.class) != null).collect(Collectors.toList()));
            clazz = clazz.getSuperclass();
        }

        return res;
    }

    public void stageLocalFiles(Path outputPath) {
        if (!GlobalApplicationOptions.getInstance().isUseRemoteDaemon()) {
            return;
        }
        localFiles().forEach(f -> {
            try {
                f.setAccessible(true);
                String fContent = (String) f.get(this);
                if (fContent != null) {
                    CRC32 crc = new CRC32();
                    crc.update(fContent.getBytes());
                    String fPath = Paths
                            .get(outputPath.toAbsolutePath().toString(), Long.toHexString(crc.getValue()))
                            .toString();
                    FileUtils.writeStringToFile(new File(fPath), fContent);
                    f.set(this, fPath);
                }
            } catch (IllegalAccessException | IOException e) {
                throw new RuntimeException("Failed to get local files for node " + this.getNodeName(), e);
            } finally {
                f.setAccessible(false);
            }
        });
    }

    public void recursiveConsume(Consumer<Node> consumer) {
        consumer.accept(this);

        NodeIterator children = getChildren();
        Node child = children.getNext();
        while (child != null) {
            child.recursiveConsume(consumer);
            child = children.getNext();
        }
    }

    /**
     * @param clazz the class to check against.
     * @return a NodeMatcher that matches all nodes of given clazz.
     */
    static public NodeMatcher thisNodeAcceptor(Class clazz) {
        return new NodeMatcher() {
            @Override
            public boolean matches(Node n) {
                return clazz.isAssignableFrom(n.getClass());
            }

            @Override
            public String getName() {
                return "Match against [" + clazz.getSimpleName() + ":*]";
            }
        };
    }

    /**
     * @param clazz the class to check against.
     * @param name is the name to match on.
     * @return a NodeMatcher that matches all nodes of given clazz that also have the right name.
     */
    static public NodeMatcher namedNodeAcceptor(Class clazz, String name) {
        return new NodeMatcher() {
            @Override
            public boolean matches(Node n) {
                return clazz.isAssignableFrom(n.getClass()) && n.getNodeName().equals(name);
            }

            @Override
            public String getName() {
                return "Match against [" + clazz.getSimpleName() + ":" + name + "]";
            }
        };
    }

    public String debugName() {
        return "[" + getClass().getSimpleName() + ":" + getNodeName() + "]";
    }

    private Map<String, Object> serializedNonNodeFields() {
        List<Field> fields = Arrays.stream(this.getClass().getDeclaredFields()).filter(f -> {
            return (!(Node.class.isAssignableFrom(f.getType()) || List.class.isAssignableFrom(f.getType())
                    || Map.class.isAssignableFrom(f.getType()) || f.getAnnotation(JsonIgnore.class) != null));
        }).collect(Collectors.toList());

        Map<String, Object> res = new HashMap<>();
        for (Field field : fields) {
            field.setAccessible(true);
            try {
                res.put(field.getName(), field.get(this));
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Failed to read field " + field.getName() + " in node " + getNodeName());
            }
            field.setAccessible(false);
        }

        return res;
    }

    private Map<String, Node> serializedNodeFields() {
        Map<String, Node> res = new HashMap<>();

        NodeIterator iterator = this.getChildren();
        Node next = iterator.getNext();

        while (next != null) {
            res.put(next.getNodeName(), next);
            next = iterator.getNext();
        }

        return res;
    }

    public NodeDiff diff(Node other) {
        String tt = this.getClass().getSimpleName();
        String to = other.getClass().getSimpleName();
        if (!tt.equals(to)) {
            throw new RuntimeException("Invalid comparision between unequal node types (" + tt + " != " + to + ")");
        }

        String nnt = this.getNodeName();
        String nno = other.getNodeName();
        if (!nnt.equals(nno)) {
            throw new RuntimeException("Invalid comparision between different nodes (" + nnt + " != " + nno + ")");
        }

        NodeDiff result = new NodeDiff().setChangeType(EDITED).setNode(this);

        Map<String, Object> fts = this.serializedNonNodeFields();
        Map<String, Object> fos = other.serializedNonNodeFields();

        for (Map.Entry<String, Object> entry : fts.entrySet()) {
            String fnt = entry.getKey();
            Object ot = entry.getValue();
            Object oo = fos.get(fnt);

            if (ot == null && oo == null) {
                continue;
            }

            if (ot != null && oo != null && ot.equals(oo)) {
                continue;
            }

            NodeDiff.FieldDiff fc = new NodeDiff.FieldDiff().setFieldName(fnt).setNewValue(ot).setOldValue(oo);

            result.addFieldDiff(fc);
        }

        Map<String, Node> nts = this.serializedNodeFields();
        Map<String, Node> nos = other.serializedNodeFields();

        for (Map.Entry<String, Node> entry : nts.entrySet()) {
            nnt = entry.getKey();
            Node nt = entry.getValue();
            Node no = nos.get(nnt);

            if (no == null) {
                result.addNodeDiff(new NodeDiff().setChangeType(ADDED).setNode(nt));
                continue;
            }

            NodeDiff diff = nt.diff(no);

            if (diff != null) {
                result.addNodeDiff(diff);
            }
        }

        for (Map.Entry<String, Node> entry : nos.entrySet()) {
            nno = entry.getKey();
            Node no = entry.getValue();
            Node nt = nts.get(nno);

            if (nt == null) {
                result.addNodeDiff(new NodeDiff().setChangeType(REMOVED).setNode(no));
            }
        }

        if (result.getFieldDiffs().isEmpty() && result.getNodeDiffs().isEmpty()) {
            return null;
        }
        return result;
    }

    private void swapLocalFilePrefixes(String to, String from) {
        Consumer<Node> fileFinder = n -> n.localFiles().forEach(f -> {
            try {
                f.setAccessible(true);
                String fPath = (String) f.get(n);
                if (fPath == null) {
                    return;
                }

                if (fPath.startsWith(to)) {
                    log.info("File " + f.getName() + " was already in correct format " + fPath);
                    return;
                }

                if (!fPath.startsWith(from)) {
                    throw new HalException(FATAL,
                            "Local file: " + fPath + " has incorrect prefix - must match " + from);
                }

                fPath = to + fPath.substring(from.length());
                f.set(n, fPath);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Failed to get local files for node " + n.getNodeName(), e);
            } finally {
                f.setAccessible(false);
            }
        });

        recursiveConsume(fileFinder);
    }

    public void makeLocalFilesRelative(String halconfigPath) {
        swapLocalFilePrefixes(LocalFile.RELATIVE_PATH_PLACEHOLDER, halconfigPath);
    }

    public void makeLocalFilesAbsolute(String halconfigPath) {
        swapLocalFilePrefixes(halconfigPath, LocalFile.RELATIVE_PATH_PLACEHOLDER);
    }

    public List<String> backupLocalFiles(String outputPath) {
        List<String> files = new ArrayList<>();

        Consumer<Node> fileFinder = n -> files.addAll(n.localFiles().stream().map(f -> {
            try {
                f.setAccessible(true);
                String fPath = (String) f.get(n);
                if (fPath == null) {
                    try {
                        fPath = (String) n.getClass().getMethod("get" + StringUtils.capitalize(f.getName()))
                                .invoke(n);
                    } catch (NoSuchMethodException | InvocationTargetException ignored) {
                    }
                }

                if (fPath == null) {
                    return null;
                }

                File fFile = new File(fPath);
                String fName = fFile.getName();

                // Hash the path to uniquely flatten all files into the output directory
                Path newName = Paths.get(outputPath, Math.abs(fPath.hashCode()) + "-" + fName);
                File parent = newName.toFile().getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                } else if (fFile.getParent().equals(parent.toString())) {
                    // Don't move paths that are already in the right folder
                    return fPath;
                }
                Files.copy(Paths.get(fPath), newName, REPLACE_EXISTING);

                f.set(n, newName.toString());
                return newName.toString();
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Failed to get local files for node " + n.getNodeName(), e);
            } catch (IOException e) {
                throw new HalException(FATAL, "Failed to backup user file: " + e.getMessage(), e);
            } finally {
                f.setAccessible(false);
            }
        }).filter(Objects::nonNull).collect(Collectors.toList()));
        recursiveConsume(fileFinder);

        return files;
    }

    public <T> T cloneNode(Class<T> tClass) {
        ObjectMapper mapper = new ObjectMapper();
        return mapper.convertValue(mapper.convertValue(this, Map.class), tClass);
    }
}