001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bstefanescu, jcarsique
018 */
019package org.nuxeo.connect.update.task.standalone;
020
021import static java.nio.charset.StandardCharsets.UTF_8;
022
023import java.io.File;
024import java.io.IOException;
025import java.io.Reader;
026import java.io.StringReader;
027import java.util.ArrayList;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031
032import javax.xml.parsers.DocumentBuilder;
033import javax.xml.parsers.DocumentBuilderFactory;
034import javax.xml.parsers.ParserConfigurationException;
035
036import org.apache.commons.io.FileUtils;
037import org.nuxeo.connect.update.LocalPackage;
038import org.nuxeo.connect.update.PackageException;
039import org.nuxeo.connect.update.PackageUpdateService;
040import org.nuxeo.connect.update.ValidationStatus;
041import org.nuxeo.connect.update.task.Command;
042import org.nuxeo.connect.update.xml.XmlWriter;
043import org.w3c.dom.Document;
044import org.w3c.dom.Element;
045import org.w3c.dom.Node;
046import org.xml.sax.InputSource;
047import org.xml.sax.SAXException;
048
049/**
050 * A command based task.
051 *
052 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
053 */
054public abstract class CommandsTask extends AbstractTask {
055
056    protected final List<Command> commands;
057
058    /**
059     * The log is generated in the inverse order of commands to ensure last command is rollbacked first.
060     */
061    protected final LinkedList<Command> commandLog;
062
063    public CommandsTask(PackageUpdateService pus) {
064        super(pus);
065        commands = new ArrayList<>();
066        commandLog = new LinkedList<>();
067    }
068
069    /**
070     * Get the commands file from where to load commands for this task.
071     */
072    protected abstract File getCommandsFile() throws PackageException;
073
074    @SuppressWarnings("hiding")
075    @Override
076    public void initialize(LocalPackage pkg, boolean restart) throws PackageException {
077        super.initialize(pkg, restart);
078        loadCommands();
079    }
080
081    /**
082     * Load the commands of this task given the user parameters. The parameter map may be null.
083     */
084    protected void loadCommands() throws PackageException {
085        try {
086            String content = loadParametrizedFile(getCommandsFile(), env);
087            StringReader reader = new StringReader(content);
088            readLog(reader);
089        } catch (IOException e) {
090            throw new PackageException("Failed to load commands file", e);
091        }
092    }
093
094    /**
095     * Gets the commands to execute.
096     */
097    public List<Command> getCommands() {
098        return commands;
099    }
100
101    /**
102     * Gets the command log. These are the commands ran so far.
103     */
104    public List<Command> getCommandLog() {
105        return commandLog;
106    }
107
108    /**
109     * Adds a command to this task.
110     */
111    public void addCommand(Command command) {
112        commands.add(command);
113    }
114
115    /**
116     * User parameters are not handled by default. You need to implement your own task to do this.
117     */
118    @Override
119    protected void doRun(Map<String, String> params) throws PackageException {
120        for (Command cmd : commands) {
121            Command rollbackCmd = cmd.run(this, params);
122            if (rollbackCmd != null) {
123                if (rollbackCmd.isPostInstall()) {
124                    commandLog.add(rollbackCmd);
125                } else {
126                    commandLog.addFirst(rollbackCmd);
127                }
128            }
129        }
130        // XXX: force a flush?
131        flush();
132    }
133
134    /**
135     * @since 5.6
136     */
137    protected abstract void flush() throws PackageException;
138
139    @Override
140    protected void doRollback() throws PackageException {
141        while (!commandLog.isEmpty()) {
142            commandLog.removeFirst().run(this, null);
143        }
144    }
145
146    @Override
147    public void doValidate(ValidationStatus status) throws PackageException {
148        // the target platform is not checked at install
149        // check that commands can be run
150        for (Command cmd : commands) {
151            cmd.validate(this, status);
152        }
153    }
154
155    public void writeLog(File file) throws PackageException {
156        XmlWriter writer = new XmlWriter();
157        writer.start("uninstall");
158        writer.startContent();
159        for (Command cmd : commandLog) {
160            cmd.writeTo(writer);
161        }
162        writer.end("uninstall");
163        try {
164            // replace all occurrences of the installation path with the corresponding variable otherwise the uninstall
165            // will not work after renaming the installation directory
166            String content = parametrizePaths(writer.toString());
167            // replace '//' by '/' if any
168            content = content.replace(File.separator.concat(File.separator), File.separator);
169            FileUtils.writeStringToFile(file, content, UTF_8);
170        } catch (IOException e) {
171            throw new PackageException("Failed to write commands", e);
172        }
173    }
174
175    public String parametrizePaths(String content) {
176        return content.replace(serverPathPrefix, "${" + ENV_SERVER_HOME + "}/");
177    }
178
179    public void readLog(Reader reader) throws PackageException {
180        try {
181            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
182            factory.setNamespaceAware(true);
183            DocumentBuilder builder = factory.newDocumentBuilder();
184            Document document = builder.parse(new InputSource(reader));
185            Element root = document.getDocumentElement();
186            Node node = root.getFirstChild();
187            while (node != null) {
188                if (node.getNodeType() == Node.ELEMENT_NODE) {
189                    Element element = (Element) node;
190                    String id = node.getNodeName();
191                    Command cmd = service.getCommand(id);
192                    if (cmd == null) { // may be the name of an embedded class
193                        try {
194                            cmd = (Command) pkg.getData().loadClass(id).getConstructor().newInstance();
195                        } catch (ReflectiveOperationException t) {
196                            throw new PackageException("Unknown command: " + id);
197                        }
198                    }
199                    cmd.initialize(element);
200                    cmd.setPackageUpdateService(service);
201                    commands.add(cmd);
202                }
203                node = node.getNextSibling();
204            }
205        } catch (ParserConfigurationException | SAXException | IOException e) {
206            throw new PackageException("Failed to read commands", e);
207        }
208    }
209
210}