net.oneandone.stool.SystemImport.java Source code

Java tutorial

Introduction

Here is the source code for net.oneandone.stool.SystemImport.java

Source

/**
 * Copyright 1&1 Internet AG, https://github.com/1and1/
 *
 * 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 net.oneandone.stool;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.internal.Streams;
import com.google.gson.stream.JsonWriter;
import net.oneandone.stool.configuration.Bedroom;
import net.oneandone.stool.stage.Stage;
import net.oneandone.stool.util.Files;
import net.oneandone.stool.util.Session;
import net.oneandone.sushi.cli.ArgumentException;
import net.oneandone.sushi.cli.Option;
import net.oneandone.sushi.cli.Remaining;
import net.oneandone.sushi.cli.Value;
import net.oneandone.sushi.fs.file.FileNode;
import net.oneandone.sushi.util.Diff;
import net.oneandone.sushi.util.Strings;

import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class SystemImport extends SessionCommand {
    public static enum Include {
        ALL(true, true), CONFIG(true, false), STAGES(false, true);

        public final boolean withConfig;
        public final boolean withStages;

        private Include(boolean withConfig, boolean withStages) {
            this.withConfig = withConfig;
            this.withStages = withStages;
        }
    }

    public SystemImport(Session session) {
        this(session, null);
    }

    public SystemImport(Session session, FileNode oldHome) {
        super(session);
        this.oldHome = oldHome;
    }

    @Value(name = "oldHome", position = 1)
    private FileNode oldHome;

    @Option("include")
    private Include include = Include.ALL;

    private final List<FileNode> oldWrappers = new ArrayList<>();

    @Remaining
    public void select(String name) {
        FileNode wrapper;

        wrapper = oldHome.join("wrappers", name);
        if (!wrapper.isDirectory()) {
            throw new ArgumentException("old stage not found: " + wrapper.getAbsolute());
        }
        oldWrappers.add(wrapper);
    }

    @Override
    public void doInvoke() throws Exception {
        Bedroom oldBedroom;
        final Bedroom newBedroom;
        String newBedroomOrig;
        String name;
        List<Patch> patches;
        Iterator<Patch> iter;
        Patch patch;

        oldHome.checkDirectory();
        if (oldHome.equals(session.home)) {
            throw new ArgumentException("cannot import from myself");
        }
        console.info.println();
        console.info.println("CAUTION: import has the following limitations:");
        console.info.println("  * 'defaults' are not migrated");
        console.info.println("  * 'portPrefixes' are gone, your stage will run under a new port");
        console.info.println();
        oldBedroom = Bedroom.loadOrCreate(oldHome);
        newBedroom = Bedroom.loadOrCreate(session.home);
        newBedroomOrig = newBedroom.stages().toString();
        patches = new ArrayList<>();
        if (include.withConfig) {
            patches.add(stoolConfig());
        }
        if (include.withStages) {
            if (oldWrappers.isEmpty()) {
                for (FileNode oldWrapper : oldHome.join("wrappers").list()) {
                    name = oldWrapper.getName();
                    if (session.wrappers.join(name).exists()) {
                        console.info.println("ignoring stage that already exists: " + name);
                    } else {
                        oldWrappers.add(oldWrapper);
                    }
                }
            }
            for (FileNode oldWrapper : oldWrappers) {
                name = oldWrapper.getName();
                if (oldBedroom.stages().contains(name)) {
                    newBedroom.add(name);
                }
                patches.add(stage(oldWrapper));
            }
        }
        patches.add(new Patch("M " + newBedroom.file(), Diff.diff(newBedroomOrig, newBedroom.stages().toString())) {
            @Override
            public void apply() throws IOException {
                newBedroom.save();
            }
        });
        iter = patches.iterator();
        while (iter.hasNext()) {
            patch = iter.next();
            if (patch.message.isEmpty()) {
                iter.remove();
            } else {
                console.info.println(patch.header);
                console.info.println(Strings.indent(patch.message, "  "));
            }
        }
        if (patches.isEmpty()) {
            console.info.println("nothing to do.");
        } else {
            console.pressReturn();
            for (Patch p : patches) {
                p.apply();
            }
        }
    }

    private Patch stoolConfig() throws IOException {
        String current;
        final String path = "config.json";
        final FileNode dest;
        final String result;
        String diff;

        dest = session.home.join(path);
        current = dest.readString();
        result = mergeConfig(oldHome.join(path).readString(), current, new Object() {
            void sharedBinRemove() {
            }

            void versionRemove() {
            }

            void stagesRemove() {
            }

            // because values are all-strings no (e.g. in tomcat.select)
            void defaultsRemove() {
            }

            String portPrefixFirstRename() {
                return "portFirst";
            }

            JsonElement portPrefixFirstTransform(JsonElement orig) {
                return new JsonPrimitive(orig.getAsInt() * 10);
            }

            String portPrefixLastRename() {
                return "portLast";
            }

            JsonElement portPrefixLastTransform(JsonElement orig) {
                return new JsonPrimitive(orig.getAsInt() * 10 + 9);
            }

            String autoremoveRename() {
                return "autoRemove";
            }
        });
        diff = Diff.diff(current, result);
        return new Patch("M " + dest.getAbsolute(), diff) {
            public void apply() throws IOException {
                dest.writeString(result);
            }
        };
    }

    private Patch stage(FileNode oldWrapper) throws IOException {
        final FileNode tmpWrapper;
        final FileNode destWrapper;
        FileNode directory;
        final Stage stage;
        String url;
        String msg;
        FileNode tmpConfig;
        String tmp;

        directory = (FileNode) oldWrapper.join("anchor").resolveLink();
        directory.checkDirectory();
        url = Stage.probe(session.subversion(), directory);
        destWrapper = session.wrappers.join(oldWrapper.getName());
        destWrapper.checkNotExists();
        // Temp Wrapper in wrapper directory, because it fasted to move within the same filesystem.
        // And Sushi has problems to move the anchor symlink across file systems
        tmpWrapper = session.wrappers.createTempDirectory();
        Files.stoolDirectory(tmpWrapper);
        stage = Stage.createOpt(session, url, session.createStageConfiguration(url), tmpWrapper, directory);
        stage.tuneConfiguration();
        stage.saveWrapper();
        stage.getDirectory().link(stage.anchor());
        tmpConfig = tmpWrapper.join("config.json");
        tmp = tmpConfig.readString();
        tmp = mergeConfig(oldWrapper.join("config.json").readString(), tmp,
                // 2.13 -> 3.0 changes
                new Object() {
                    void portPrefixRemove() {
                    }
                });
        tmpConfig.writeString(tmp);
        msg = Diff.diff(oldWrapper.join("config.json").readString(), tmp);
        if (msg.isEmpty()) {
            // make sure the message is not empty, because we have to move the file
            msg = "(no config changes)";
        }
        return new Patch("A " + destWrapper.getAbsolute(), msg) {
            @Override
            public void apply() throws IOException {
                tmpWrapper.move(destWrapper);
                if (session.configuration.security.isShared()) {
                    // TODO: I cannot use the chown command because sudo is not allowed for new home
                    session.sudo("chown", "-R", stage.owner(), destWrapper.getAbsolute());
                }
            }
        };
    }

    public static String mergeConfig(String srcString, String destString, Object mapper) throws IOException {
        JsonParser parser;
        JsonObject src;
        JsonObject dest;
        Object[] mapped;

        parser = new JsonParser();
        src = (JsonObject) parser.parse(srcString);
        dest = (JsonObject) parser.parse(destString);
        for (Map.Entry<String, JsonElement> entry : src.entrySet()) {
            mapped = map(mapper, entry.getKey(), entry.getValue());
            if (mapped != null) {
                dest.add((String) mapped[0], (JsonElement) mapped[1]);
            }
        }
        return toString(dest);
    }

    private static Object[] map(Object mapper, String name, JsonElement value) {
        Class clazz;
        Method rename;
        Method transform;

        clazz = mapper.getClass();
        if (method(clazz, name + "Remove") != null) {
            return null;
        }
        rename = method(clazz, name + "Rename");
        transform = method(clazz, name + "Transform", JsonElement.class);
        return new Object[] { rename(rename, mapper, name), transform(transform, mapper, value) };
    }

    /* Search with method by name, check arguments later. Helps to detect methods with wrong arguments. */
    private static Method method(Class clazz, String name, Class... args) {
        Method result;

        result = null;
        for (Method m : clazz.getDeclaredMethods()) {
            if (m.getName().equals(name)) {
                if (result != null) {
                    throw new ArgumentException("method ambiguous: " + name);
                }
                result = m;
            }
        }
        if (result != null) {
            if (!Arrays.equals(args, result.getParameterTypes())) {
                throw new ArgumentException("argument type mismatch for method " + name);
            }
        }
        return result;
    }

    private static String rename(Method method, Object object, String old) {
        if (method == null) {
            return old;
        } else {
            try {
                return (String) method.invoke(object);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new IllegalStateException(e);
            }
        }
    }

    private static JsonElement transform(Method method, Object object, JsonElement old) {
        if (method == null) {
            return old;
        } else {
            try {
                return (JsonElement) method.invoke(object, old);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new IllegalStateException(e);
            }
        }
    }

    public static String toString(JsonObject obj) {
        try {
            StringWriter stringWriter = new StringWriter();
            JsonWriter jsonWriter = new JsonWriter(stringWriter);
            jsonWriter.setIndent("  ");
            jsonWriter.setLenient(true);
            Streams.write(obj, jsonWriter);
            return stringWriter.toString();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    public abstract static class Patch {
        public final String header;
        public final String message;

        public Patch(String header, String message) {
            this.header = header;
            this.message = message;
        }

        public abstract void apply() throws IOException;
    }
}