com.facebook.buck.apple.project_generator.WorkspaceGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.apple.project_generator.WorkspaceGenerator.java

Source

/*
 * Copyright 2014-present Facebook, 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.facebook.buck.apple.project_generator;

import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.MoreProjectFilesystems;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;

import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.SortedMap;
import java.util.Stack;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

/**
 * Collects file references and generates an xcworkspace.
 */
class WorkspaceGenerator {
    private final ProjectFilesystem projectFilesystem;
    private final String workspaceName;
    private final Path outputDirectory;
    private final SortedMap<String, WorkspaceNode> children;

    private static class WorkspaceNode {

    }

    private static class WorkspaceGroup extends WorkspaceNode {
        private final SortedMap<String, WorkspaceNode> children;

        WorkspaceGroup() {
            this.children = Maps.newTreeMap();
        }

        public Map<String, WorkspaceNode> getChildren() {
            return children;
        }
    }

    private static class WorkspaceFileRef extends WorkspaceNode {
        private final Path path;

        WorkspaceFileRef(Path path) {
            this.path = path;
        }

        public Path getPath() {
            return path;
        }
    }

    public WorkspaceGenerator(ProjectFilesystem projectFilesystem, String workspaceName, Path outputDirectory) {
        this.projectFilesystem = projectFilesystem;
        this.workspaceName = workspaceName;
        this.outputDirectory = outputDirectory;
        this.children = Maps.newTreeMap();
    }

    /**
     * Adds a reference to a project file to the generated workspace.
     *
     * @param path Path to the referenced project file in the repository.
     */
    public void addFilePath(Path path) {
        path = path.normalize();

        Optional<Path> groupPath = Optional.empty();
        // We skip the last name before the file name as it's usually the same as the project name, and
        // adding a group would add an unnecessary level of nesting. We don't check whether it's the
        // same or not to avoid inconsistent behaviour: this will result in all projects to show up in a
        // group with the path of their grandparent directory in all cases.
        if (path.getNameCount() > 2) {
            groupPath = Optional.of(path.subpath(0, path.getNameCount() - 2));
        }
        addFilePath(path, groupPath);
    }

    /**
     * Adds a reference to a project file to the group hierarchy of the generated workspace.
     *
     * @param path Path to the referenced project file in the repository.
     * @param groupPath Path in the group hierarchy of the generated workspace where
     *                  the reference will be placed.
     *                  If absent, the project reference is placed to the root of the workspace.
     */
    public void addFilePath(Path path, Optional<Path> groupPath) {
        Map<String, WorkspaceNode> children = this.children;
        if (groupPath.isPresent()) {
            for (Path groupPathPart : groupPath.get()) {
                String groupName = groupPathPart.toString();
                WorkspaceNode node = children.get(groupName);
                WorkspaceGroup group;
                if (node instanceof WorkspaceFileRef) {
                    throw new HumanReadableException(
                            "Invalid workspace: a group and a project have the same name: %s", groupName);
                } else if (node == null) {
                    group = new WorkspaceGroup();
                    children.put(groupName, group);
                } else if (node instanceof WorkspaceGroup) {
                    group = (WorkspaceGroup) node;
                } else {
                    // Unreachable
                    throw new HumanReadableException(
                            "Expected a workspace to only contain groups and file references");
                }
                children = group.getChildren();
            }
        }
        children.put(path.toString(), new WorkspaceFileRef(path));
    }

    private void walkNodeTree(FileVisitor<Map.Entry<String, WorkspaceNode>> visitor) throws IOException {
        Stack<Iterator<Map.Entry<String, WorkspaceNode>>> iterators = new Stack<>();
        Stack<Map.Entry<String, WorkspaceNode>> groups = new Stack<>();
        iterators.push(this.children.entrySet().iterator());
        while (!iterators.isEmpty()) {
            if (!iterators.peek().hasNext()) {
                if (groups.isEmpty()) {
                    break;
                }
                visitor.postVisitDirectory(groups.pop(), null);
                iterators.pop();
                continue;
            }
            Map.Entry<String, WorkspaceNode> nextEntry = iterators.peek().next();
            WorkspaceNode nextNode = nextEntry.getValue();
            if (nextNode instanceof WorkspaceGroup) {
                visitor.preVisitDirectory(nextEntry, null);
                WorkspaceGroup nextGroup = (WorkspaceGroup) nextNode;
                groups.push(nextEntry);
                iterators.push(nextGroup.getChildren().entrySet().iterator());
            } else if (nextNode instanceof WorkspaceFileRef) {
                visitor.visitFile(nextEntry, null);
            } else {
                // Unreachable
                throw new HumanReadableException("Expected a workspace to only contain groups and file references");
            }
        }
    }

    public Path writeWorkspace() throws IOException {
        DocumentBuilder docBuilder;
        Transformer transformer;
        try {
            docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            transformer = TransformerFactory.newInstance().newTransformer();
        } catch (ParserConfigurationException | TransformerConfigurationException e) {
            throw new RuntimeException(e);
        }

        DOMImplementation domImplementation = docBuilder.getDOMImplementation();
        final Document doc = domImplementation.createDocument(/* namespaceURI */ null, "Workspace",
                /* docType */ null);
        doc.setXmlVersion("1.0");

        Element rootElem = doc.getDocumentElement();
        rootElem.setAttribute("version", "1.0");

        final Stack<Element> groups = new Stack<>();
        groups.push(rootElem);

        FileVisitor<Map.Entry<String, WorkspaceNode>> visitor = new FileVisitor<Map.Entry<String, WorkspaceNode>>() {
            @Override
            public FileVisitResult preVisitDirectory(Map.Entry<String, WorkspaceNode> dir,
                    BasicFileAttributes attrs) throws IOException {
                Preconditions.checkArgument(dir.getValue() instanceof WorkspaceGroup);
                Element element = doc.createElement("Group");
                element.setAttribute("location", "container:");
                element.setAttribute("name", dir.getKey());
                groups.peek().appendChild(element);
                groups.push(element);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Map.Entry<String, WorkspaceNode> file, BasicFileAttributes attrs)
                    throws IOException {
                Preconditions.checkArgument(file.getValue() instanceof WorkspaceFileRef);
                WorkspaceFileRef fileRef = (WorkspaceFileRef) file.getValue();
                Element element = doc.createElement("FileRef");
                element.setAttribute("location", "container:"
                        + MorePaths.relativize(MorePaths.normalize(outputDirectory), fileRef.getPath()).toString());
                groups.peek().appendChild(element);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Map.Entry<String, WorkspaceNode> file, IOException exc)
                    throws IOException {
                return FileVisitResult.TERMINATE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Map.Entry<String, WorkspaceNode> dir, IOException exc)
                    throws IOException {
                groups.pop();
                return FileVisitResult.CONTINUE;
            }
        };

        walkNodeTree(visitor);

        Path projectWorkspaceDir = outputDirectory.resolve(workspaceName + ".xcworkspace");
        projectFilesystem.mkdirs(projectWorkspaceDir);
        Path serializedWorkspace = projectWorkspaceDir.resolve("contents.xcworkspacedata");
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            DOMSource source = new DOMSource(doc);
            StreamResult result = new StreamResult(outputStream);
            transformer.transform(source, result);
            String contentsToWrite = outputStream.toString();
            if (MoreProjectFilesystems.fileContentsDiffer(
                    new ByteArrayInputStream(contentsToWrite.getBytes(Charsets.UTF_8)), serializedWorkspace,
                    projectFilesystem)) {
                projectFilesystem.writeContentsToPath(contentsToWrite, serializedWorkspace);
            }
        } catch (TransformerException e) {
            throw new RuntimeException(e);
        }
        Path xcshareddata = projectWorkspaceDir.resolve("xcshareddata");
        projectFilesystem.mkdirs(xcshareddata);
        Path workspaceSettingsPath = xcshareddata.resolve("WorkspaceSettings.xcsettings");
        String workspaceSettings = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                + "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\""
                + " \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" + "<plist version=\"1.0\">\n"
                + "<dict>\n" + "\t<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>\n"
                + "\t<false/>\n" + "</dict>\n" + "</plist>";
        projectFilesystem.writeContentsToPath(workspaceSettings, workspaceSettingsPath);
        return projectWorkspaceDir;
    }
}