/*
* 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 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 java.util.TreeMap;
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;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/** 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 = new TreeMap<>();
}
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 = new TreeMap<>();
}
/**
* 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 getWorkspaceDir() {
return outputDirectory.resolve(workspaceName + ".xcworkspace");
}
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 = getWorkspaceDir();
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;
}
}