blob: d3c60aa692c3ee04a990d6cbc578557e4eceba12 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018 SAP SE and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* SAP SE - initial version
*******************************************************************************/
package org.eclipse.urischeme.internal.registration;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Used to change the CFBundleURLTypes property of a Mac .plist file. Adds
* handler entries for uri schemes in "CFBundleURLSchemes" elements. Can also
* remove schemes.
*/
public class PlistFileWriter {
private static final String XPATH_PLIST_DICT_CF_BUNDLE_URL_TYPES_KEY = "/plist/dict/key[text()=\"CFBundleURLTypes\"]"; //$NON-NLS-1$
private static final String XPATH_PLIST_DICT_CF_BUNDLE_URL_TYPES_ARRAY = XPATH_PLIST_DICT_CF_BUNDLE_URL_TYPES_KEY
+ "/following-sibling::array"; //$NON-NLS-1$
private static final String ELEMENT_NAME_KEY = "key"; //$NON-NLS-1$
private static final String ELEMENT_NAME_ARRAY = "array"; //$NON-NLS-1$
private static final String ELEMENT_NAME_STRING = "string"; //$NON-NLS-1$
private static final String ELEMENT_NAME_DICT = "dict"; //$NON-NLS-1$
private static final String KEY_VALUE_CF_BUNDLE_URL_TYPES = "CFBundleURLTypes"; //$NON-NLS-1$
private static final String KEY_VALUE_CF_BUNDLE_URL_NAME = "CFBundleURLName"; //$NON-NLS-1$
private static final String KEY_VALUE_CF_BUNDLE_URL_SCHEMES = "CFBundleURLSchemes"; //$NON-NLS-1$
private Document document;
private Element array;
/**
* Creates an instance of the PlistFileWriter. Throws an
* {@link IllegalStateException} if the given {@link Reader} does not provide
* .plist file.
*
* @param reader The file reader of the .plist file
*
* @throws IllegalArgumentException if file cannot be understood as .plist file
*/
public PlistFileWriter(Reader reader) {
this.document = getDom(reader);
this.array = getOrCreateBundleUrlTypesAndArray();
}
/**
* Checks if the given scheme is registered in this .plist file
*
* @param scheme that should be checked for registration
* @return <code>true</code> if scheme registered; <code>false</code> otherwise
*/
public boolean isRegisteredScheme(String scheme) {
Util.assertUriSchemeIsLegal(scheme);
return getExistingElementFor(scheme) != null;
}
/**
* Adds an entry for the given scheme in the CFBundleURLSchemes element of the
* .plist file. Creates the CFBundleURLTypes element if not yet existing.
* Otherwise adds CFBundleURLSchemes element.
*
* @param scheme The uri scheme which should be handled by the
* application of the .plist file
* @param schemeDescription The human readable description of the scheme
*
* @throws IllegalArgumentException if the given scheme contains illegal
* characters
*
* @see #removeScheme(String)
*
* @see <a href= "https://tools.ietf.org/html/rfc3986#section-3.1">Uniform
* Resource Identifier (URI): Generic Syntax</a>
*
*/
public void addScheme(String scheme, String schemeDescription) {
// check precondition
Util.assertUriSchemeIsLegal(scheme);
if (getExistingElementFor(scheme) != null) {
return;
}
// add dict element
addIndent(array, 3);
Element dictInArray = addChildNode(array, ELEMENT_NAME_DICT, null);
// add key CFBundleURLName
addIndent(dictInArray, 4);
addChildNode(dictInArray, ELEMENT_NAME_KEY, KEY_VALUE_CF_BUNDLE_URL_NAME);
// add string
addIndent(dictInArray, 5);
addChildNode(dictInArray, ELEMENT_NAME_STRING, schemeDescription);
// add key CFBundleURLSchemes
addIndent(dictInArray, 4);
addChildNode(dictInArray, ELEMENT_NAME_KEY, KEY_VALUE_CF_BUNDLE_URL_SCHEMES);
// add array
addIndent(dictInArray, 5);
Element schemeArray = addChildNode(dictInArray, ELEMENT_NAME_ARRAY, null);
// add string
addIndent(schemeArray, 6);
addChildNode(schemeArray, ELEMENT_NAME_STRING, scheme);
// indent closing tags
addIndent(schemeArray, 5);
addIndent(dictInArray, 3);
}
/**
* Removes the corresponding CFBundleURLSchemes element for the given scheme
* from the CFBundleURLTypes element of the .plist file. Removes the
* CFBundleURLTypes element completely if it is empty (no handled schemes) after
* removal.
*
* @param scheme The uri scheme which should not be handled anymore by the
* application of the .plist file.
*
* @throws IllegalArgumentException if the given scheme contains illegal
* characters
*
* @see #addScheme(String, String)
*
* @see <a href=
* "https://tools.ietf.org/html/rfc3986#section-3.1">https://tools.ietf.org/html/rfc3986#section-3.1</a>
*
*/
public void removeScheme(String scheme) {
Util.assertUriSchemeIsLegal(scheme);
Element dict = getExistingElementFor(scheme);
if (dict == null) {
// not found, no need to remove
return;
}
Node arrayNode = dict.getParentNode();
removeTextNode(arrayNode, dict.getPreviousSibling()); // remove tab and line break before dict
arrayNode.removeChild(dict);
}
/**
* Writes the content (xml) of the .plist file to the given {@link Writer}
*
* @param writer The Writer to which the xml should be written to, e.g.
* {@link BufferedWriter}
*
*/
public void writeTo(Writer writer) {
boolean hasDict = false;
for (int i = 0; i < array.getChildNodes().getLength(); i++) {
Node child = array.getChildNodes().item(i);
if ("dict".equals(child.getNodeName())) { //$NON-NLS-1$
hasDict = true;
break;
}
}
// check if no schemes in cfbundlurltypes
if (!hasDict) {
Node keyNode = evaluateXpathOnElement(document, XPATH_PLIST_DICT_CF_BUNDLE_URL_TYPES_KEY);
if (keyNode != null) {
keyNode.getParentNode().removeChild(keyNode);
array.getParentNode().removeChild(array);
}
} else {
// indent last closing tag
addIndent(array, 2);
}
transformDocument(writer);
}
private void transformDocument(Writer writer) {
try {
DOMSource source = new DOMSource(this.document);
TransformerFactory.newInstance().newTransformer().transform(source, new StreamResult(writer));
} catch (TransformerException e) {
throw new IllegalStateException(e);
} finally {
close(writer);
}
}
private Document getDom(Reader reader) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new InputSource(reader));
} catch (ParserConfigurationException | IOException | SAXException e) {
throw new IllegalArgumentException(e);
} finally {
close(reader);
}
}
private void close(Closeable closeable) {
try {
closeable.close();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private Element getOrCreateBundleUrlTypesAndArray() {
Element arrayNode = evaluateXpathOnElement(this.document, XPATH_PLIST_DICT_CF_BUNDLE_URL_TYPES_ARRAY);
if (arrayNode != null) {
// adapt whitespace of array
if (removeTextNode(arrayNode, arrayNode.getLastChild())) {
addLineBreak(arrayNode);
}
} else {
// create CFBundleURLTypes and it's array
Element plistElement = document.getDocumentElement();
NodeList dictElements = plistElement.getElementsByTagName(ELEMENT_NAME_DICT);
if (dictElements.getLength() == 0) {
throw new IllegalStateException("Top level 'DICT' element could not be found"); //$NON-NLS-1$
}
Node dictElement = dictElements.item(0);
addIndent(dictElement, 1);
addChildNode(dictElement, ELEMENT_NAME_KEY, KEY_VALUE_CF_BUNDLE_URL_TYPES);
addIndent(dictElement, 2);
arrayNode = addChildNode(dictElement, ELEMENT_NAME_ARRAY, null);
}
return arrayNode;
}
private boolean removeTextNode(Node parent, Node textNode) {
if (textNode instanceof Text) {
parent.removeChild(textNode);
return true;
}
return false;
}
private Element addChildNode(Node parent, String name, String value) {
Element newElement = document.createElement(name);
if (value != null) {
newElement.appendChild(document.createTextNode(value));
} else {
addLineBreak(newElement);
}
parent.appendChild(newElement);
addLineBreak(parent);
return newElement;
}
private void addLineBreak(Node node) {
node.appendChild(document.createTextNode("\n")); //$NON-NLS-1$
}
private void addIndent(Node node, int indent) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < indent; i++) {
builder.append(" "); //$NON-NLS-1$
}
node.appendChild(document.createTextNode(builder.toString()));
}
private Element getExistingElementFor(String scheme) {
String xpathToSchemeDictElement = XPATH_PLIST_DICT_CF_BUNDLE_URL_TYPES_ARRAY
+ "/dict/key[text()=\"CFBundleURLSchemes\"]/following-sibling::array/string[text()=\"" + scheme //$NON-NLS-1$
+ "\"]/../.."; //$NON-NLS-1$
return evaluateXpathOnElement(this.document, xpathToSchemeDictElement);
}
private Element evaluateXpathOnElement(Node node, String xpath) {
try {
XPathExpression xpathExpression = XPathFactory.newInstance().newXPath().compile(xpath);
NodeList nodeList = (NodeList) xpathExpression.evaluate(node, XPathConstants.NODESET);
return nodeList.getLength() == 0 ? null : (Element) nodeList.item(0);
} catch (XPathExpressionException e) {
throw new IllegalStateException(e); // cannot happen
}
}
}