├── settings.gradle ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .travis.yml ├── CONTRIBUTING.md ├── src ├── main │ └── java │ │ └── de │ │ └── ito │ │ └── gradle │ │ └── plugin │ │ └── androidstringextractor │ │ ├── AndroidStringExtractorPlugin.java │ │ ├── internal │ │ ├── AndroidProject.java │ │ ├── XmlFileReader.java │ │ ├── Util.java │ │ ├── XmlFileWriter.java │ │ ├── AndroidProjectFactory.java │ │ ├── ReferenceReplacer.java │ │ ├── StringValues.java │ │ ├── StringOccurrence.java │ │ ├── Flavor.java │ │ ├── LayoutScanner.java │ │ ├── FlavorScanner.java │ │ ├── LayoutStringExtractor.java │ │ ├── StringValuesWriter.java │ │ ├── Layout.java │ │ ├── LayoutParser.java │ │ └── StringValuesReader.java │ │ └── AndroidStringExtractorTask.java └── test │ └── java │ └── de │ └── ito │ └── gradle │ └── plugin │ └── androidstringextractor │ ├── internal │ ├── StringOccurrenceTest.java │ ├── ReferenceReplacerTest.java │ ├── LayoutScannerTest.java │ ├── FlavorScannerTest.java │ ├── XmlFileWriterTest.java │ ├── XmlFileReaderTest.java │ ├── FlavorTest.java │ ├── StringValuesWriterTest.java │ ├── LayoutParserTest.java │ ├── LayoutTest.java │ ├── LayoutStringExtractorTest.java │ └── StringValuesReaderTest.java │ └── AndroidStringExtractorPluginTest.java ├── LICENSE ├── README.md ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'androidLayoutStringExtractor' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | build/ 4 | classes/ 5 | *.iml 6 | local.properties 7 | gradle.properties 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-objects/android-string-extractor-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | after_success: 7 | - ./gradlew jacocoTestReport coveralls 8 | 9 | branches: 10 | except: 11 | - gh-pages 12 | 13 | notifications: 14 | email: false 15 | 16 | cache: 17 | directories: 18 | - $HOME/.gradle -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Oct 26 13:06:46 CEST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | #Contributing 2 | Thank you for contributing to this project. 3 | 4 | ##Code style 5 | Before contributing to this project please make sure to install and configure this project's [code style](https://github.com/square/java-code-styles). 6 | 7 | ##Conventions 8 | When changing or adding code please pay attention to stick to current naming and architecture conventions. 9 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/AndroidStringExtractorPlugin.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor; 2 | 3 | import org.gradle.api.Plugin; 4 | import org.gradle.api.Project; 5 | 6 | public class AndroidStringExtractorPlugin implements Plugin { 7 | 8 | static final String TASK_NAME = "extractStringsFromLayouts"; 9 | 10 | @Override 11 | public void apply(Project target) { 12 | target.getTasks().create(TASK_NAME, AndroidStringExtractorTask.class); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/AndroidProject.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.util.List; 6 | 7 | class AndroidProject { 8 | private final File projectPath; 9 | private final FlavorScanner flavorScanner; 10 | 11 | AndroidProject(File projectPath, FlavorScanner flavorScanner) { 12 | this.projectPath = projectPath; 13 | this.flavorScanner = flavorScanner; 14 | } 15 | 16 | List readFlavors() throws FileNotFoundException { 17 | return flavorScanner.scan(projectPath); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/XmlFileReader.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import javax.xml.parsers.DocumentBuilder; 6 | import javax.xml.parsers.DocumentBuilderFactory; 7 | import javax.xml.parsers.ParserConfigurationException; 8 | import org.w3c.dom.Document; 9 | import org.xml.sax.SAXException; 10 | 11 | class XmlFileReader { 12 | Document read(File file) throws IOException, ParserConfigurationException, SAXException { 13 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 14 | DocumentBuilder builder = factory.newDocumentBuilder(); 15 | 16 | return builder.parse(file); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/AndroidStringExtractorTask.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor; 2 | 3 | import de.ito.gradle.plugin.androidstringextractor.internal.AndroidProjectFactory; 4 | import de.ito.gradle.plugin.androidstringextractor.internal.LayoutStringExtractor; 5 | import org.gradle.api.DefaultTask; 6 | import org.gradle.api.tasks.TaskAction; 7 | 8 | public class AndroidStringExtractorTask extends DefaultTask { 9 | 10 | private final LayoutStringExtractor layoutStringExtractor; 11 | 12 | public AndroidStringExtractorTask() { 13 | layoutStringExtractor = new LayoutStringExtractor(new AndroidProjectFactory()); 14 | } 15 | 16 | @TaskAction 17 | public void extractStringsFromLayouts() throws Exception { 18 | String projectPath = getProject().getProjectDir().getPath(); 19 | layoutStringExtractor.extract(projectPath); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/Util.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import javax.xml.parsers.DocumentBuilder; 5 | import javax.xml.parsers.DocumentBuilderFactory; 6 | import javax.xml.parsers.ParserConfigurationException; 7 | import org.w3c.dom.Document; 8 | 9 | class Util { 10 | static Document createEmptyDocument() throws ParserConfigurationException { 11 | DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 12 | DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); 13 | return documentBuilder.newDocument(); 14 | } 15 | 16 | static void assertPathIsDirectory(File path) { 17 | if (!path.isDirectory()) { 18 | throw new IllegalArgumentException(String.format("'%s' is no directory", path)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/XmlFileWriter.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import javax.xml.transform.OutputKeys; 6 | import javax.xml.transform.Transformer; 7 | import javax.xml.transform.TransformerException; 8 | import javax.xml.transform.TransformerFactory; 9 | import javax.xml.transform.dom.DOMSource; 10 | import javax.xml.transform.stream.StreamResult; 11 | import org.w3c.dom.Document; 12 | 13 | class XmlFileWriter { 14 | void write(Document document, File file) throws TransformerException, IOException { 15 | TransformerFactory transformerFactory = TransformerFactory.newInstance(); 16 | Transformer transformer = transformerFactory.newTransformer(); 17 | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); 18 | transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 19 | 20 | DOMSource source = new DOMSource(document); 21 | StreamResult result = new StreamResult(file); 22 | 23 | transformer.transform(source, result); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 it-objects GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/AndroidProjectFactory.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | 5 | public class AndroidProjectFactory { 6 | LayoutParser layoutParser; 7 | XmlFileReader xmlFileReader; 8 | XmlFileWriter xmlFileWriter; 9 | StringValuesReader stringValuesReader; 10 | StringValuesWriter stringValuesWriter; 11 | LayoutScanner layoutScanner; 12 | FlavorScanner flavorScanner; 13 | ReferenceReplacer referenceReplacer; 14 | 15 | public AndroidProjectFactory() { 16 | layoutParser = new LayoutParser(); 17 | xmlFileReader = new XmlFileReader(); 18 | xmlFileWriter = new XmlFileWriter(); 19 | referenceReplacer = new ReferenceReplacer(); 20 | stringValuesReader = new StringValuesReader(xmlFileReader); 21 | stringValuesWriter = new StringValuesWriter(xmlFileWriter); 22 | layoutScanner = 23 | new LayoutScanner(xmlFileReader, layoutParser, xmlFileWriter, referenceReplacer); 24 | flavorScanner = new FlavorScanner(stringValuesReader, stringValuesWriter, layoutScanner); 25 | } 26 | 27 | AndroidProject create(File projectPath) { 28 | return new AndroidProject(projectPath, flavorScanner); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/ReferenceReplacer.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.util.List; 4 | import javax.xml.xpath.XPathConstants; 5 | import javax.xml.xpath.XPathExpression; 6 | import javax.xml.xpath.XPathExpressionException; 7 | import javax.xml.xpath.XPathFactory; 8 | import org.w3c.dom.Document; 9 | import org.w3c.dom.Element; 10 | 11 | class ReferenceReplacer { 12 | void replaceHardCodedValues(Document document, List stringOccurrences) 13 | throws XPathExpressionException { 14 | for (StringOccurrence stringOccurrence : stringOccurrences) { 15 | Element n = findElementByAndroidId(document, stringOccurrence.getId()); 16 | if (n == null) continue; 17 | n.setAttribute("android:" + stringOccurrence.getAttribute(), stringOccurrence.getValue()); 18 | } 19 | } 20 | 21 | private Element findElementByAndroidId(Document document, String id) 22 | throws XPathExpressionException { 23 | String expression = String.format("//*[@*[name()='android:id'] = '%s']", "@+id/" + id); 24 | XPathExpression expr = XPathFactory.newInstance().newXPath().compile(expression); 25 | 26 | return (Element) expr.evaluate(document, XPathConstants.NODE); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/StringValues.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | import org.apache.commons.lang3.builder.EqualsBuilder; 6 | import org.apache.commons.lang3.builder.HashCodeBuilder; 7 | import org.apache.commons.lang3.builder.ToStringBuilder; 8 | 9 | class StringValues { 10 | private final Map values; 11 | private boolean hasChanged; 12 | 13 | StringValues() { 14 | values = new LinkedHashMap<>(); 15 | } 16 | 17 | StringValues(Map values){ 18 | this.values = values; 19 | } 20 | 21 | Map getValues() { 22 | return values; 23 | } 24 | 25 | void put(String key, String value) { 26 | values.put(key, value); 27 | hasChanged = true; 28 | } 29 | 30 | public boolean hasChanged() { 31 | return hasChanged; 32 | } 33 | 34 | @Override public boolean equals(Object o) { 35 | return EqualsBuilder.reflectionEquals(this, o, false); 36 | } 37 | 38 | @Override public int hashCode() { 39 | return HashCodeBuilder.reflectionHashCode(this, false); 40 | } 41 | 42 | @Override public String toString() { 43 | return ToStringBuilder.reflectionToString(this); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/StringOccurrenceTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.hamcrest.CoreMatchers.equalTo; 6 | import static org.hamcrest.core.Is.is; 7 | import static org.junit.Assert.assertThat; 8 | 9 | public class StringOccurrenceTest { 10 | @Test public void when_hasHardCodedValue_should_returnTrue() throws Exception { 11 | StringOccurrence occurrence = new StringOccurrence("id", "text", "value"); 12 | 13 | boolean actual = occurrence.hasHardCodedValue(); 14 | 15 | assertThat(actual, is(true)); 16 | } 17 | 18 | @Test public void when_hasReference_should_returnFalse() throws Exception { 19 | StringOccurrence occurrence = new StringOccurrence("id2", "hint", "@string/value2"); 20 | 21 | boolean actual = occurrence.hasHardCodedValue(); 22 | 23 | assertThat(actual, is(false)); 24 | } 25 | 26 | @Test public void when_replaceHardCodedValueByReference_should_containReference() 27 | throws Exception { 28 | StringOccurrence occurrence = new StringOccurrence("id", "text", "value"); 29 | 30 | occurrence.replaceHardCodedValueByReference("reference"); 31 | 32 | assertThat(occurrence.getValue(), equalTo("@string/reference")); 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/ReferenceReplacerTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.w3c.dom.Document; 8 | import org.w3c.dom.Element; 9 | 10 | import static org.hamcrest.CoreMatchers.equalTo; 11 | import static org.junit.Assert.assertThat; 12 | 13 | public class ReferenceReplacerTest { 14 | private ReferenceReplacer referenceReplacer; 15 | 16 | @Before public void setUp() throws Exception { 17 | this.referenceReplacer = new ReferenceReplacer(); 18 | } 19 | 20 | @Test public void when_replaceHardCodedValues_should_replaceHardcodedValues() throws Exception { 21 | StringOccurrence string = new StringOccurrence("id", "text", "value"); 22 | List strings = new ArrayList<>(); 23 | strings.add(string); 24 | 25 | Document document = Util.createEmptyDocument(); 26 | Element textView = document.createElement("TextView"); 27 | textView.setAttribute("xmlns:android", "http://schemas.android.com/apk/res/android"); 28 | textView.setAttribute("android:text", "hardcoded text"); 29 | textView.setAttribute("android:id", "@+id/id"); 30 | document.appendChild(textView); 31 | 32 | referenceReplacer.replaceHardCodedValues(document, strings); 33 | 34 | assertThat(textView.getAttribute("android:text"), equalTo(string.getValue())); 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/StringOccurrence.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | class StringOccurrence { 8 | private final String id; 9 | private final String attribute; 10 | private String value; 11 | 12 | StringOccurrence(String id, String attribute, String value) { 13 | this.id = id; 14 | this.attribute = attribute; 15 | this.value = value; 16 | } 17 | 18 | boolean hasHardCodedValue() { 19 | boolean isReference = value.startsWith("@string/"); 20 | boolean isDataBinding = value.startsWith("@{"); 21 | 22 | return !(isReference || isDataBinding); 23 | } 24 | 25 | String getId() { 26 | return id; 27 | } 28 | 29 | String getAttribute() { 30 | return attribute; 31 | } 32 | 33 | String getValue() { 34 | return value; 35 | } 36 | 37 | void replaceHardCodedValueByReference(String key) { 38 | value = "@string/" + key; 39 | } 40 | 41 | @Override public boolean equals(Object o) { 42 | return EqualsBuilder.reflectionEquals(this, o, false); 43 | } 44 | 45 | @Override public int hashCode() { 46 | return HashCodeBuilder.reflectionHashCode(this, false); 47 | } 48 | 49 | @Override public String toString() { 50 | return ToStringBuilder.reflectionToString(this); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ** THIS REPOSITORY IS OUTDATED SO THAT WE DECIDED TO ARCHIVE IT ** 2 | 3 | # android-string-extractor-plugin [![Build Status](https://travis-ci.org/it-objects/android-string-extractor-plugin.svg?branch=travis-coveralls)](https://travis-ci.org/it-objects/android-string-extractor-plugin) [![Coverage Status](https://coveralls.io/repos/github/it-objects/android-string-extractor-plugin/badge.svg?branch=travis-coveralls)](https://coveralls.io/github/it-objects/android-string-extractor-plugin?branch=travis-coveralls) [![Download](https://api.bintray.com/packages/it-objects/maven/de.ito.gradle.plugin%3Aandroid-string-extractor/images/download.svg) ](https://bintray.com/it-objects/maven/de.ito.gradle.plugin%3Aandroid-string-extractor/_latestVersion) 4 | Gradle plugin which automatically extracts hardcoded strings from Android layouts. 5 | 6 | The plugin scans all your flavors and layouts. It automatically extracts detected hardcoded values. Occurrences are replaced with generated references: 7 | 8 | ```xml 9 | 10 | 14 | 15 | 16 | 20 | ``` 21 | 22 | ## Usage 23 | ```groovy 24 | buildscript { 25 | repositories { 26 | jcenter() 27 | } 28 | dependencies { 29 | ... 30 | classpath 'de.ito.gradle.plugin:android-string-extractor:' 31 | } 32 | } 33 | 34 | apply plugin: 'android-string-extractor' 35 | ``` 36 | 37 | ```shell 38 | $ ./gradlew extractStringsFromLayouts 39 | ``` 40 | 41 | ## Contributing 42 | Contributing to this project is appreciated. 43 | Please check the [contribution guidelines](/CONTRIBUTING.md) for more information. 44 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/Flavor.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.List; 6 | import javax.xml.parsers.ParserConfigurationException; 7 | import javax.xml.transform.TransformerException; 8 | import org.apache.commons.lang3.builder.EqualsBuilder; 9 | import org.apache.commons.lang3.builder.HashCodeBuilder; 10 | import org.apache.commons.lang3.builder.ToStringBuilder; 11 | import org.xml.sax.SAXException; 12 | 13 | class Flavor { 14 | private final File path; 15 | private final StringValuesReader stringValuesReader; 16 | private final StringValuesWriter stringValuesWriter; 17 | private final LayoutScanner layoutScanner; 18 | 19 | Flavor(File path, StringValuesReader stringValuesReader, 20 | StringValuesWriter stringValuesWriter, LayoutScanner layoutScanner) { 21 | this.path = path; 22 | this.stringValuesReader = stringValuesReader; 23 | this.stringValuesWriter = stringValuesWriter; 24 | this.layoutScanner = layoutScanner; 25 | } 26 | 27 | StringValues readStringValues() throws IOException, SAXException, ParserConfigurationException { 28 | return stringValuesReader.read(path); 29 | } 30 | 31 | void writeStringValues(StringValues stringValues) 32 | throws IOException, TransformerException, ParserConfigurationException { 33 | stringValuesWriter.write(stringValues, path); 34 | } 35 | 36 | List readLayouts() { 37 | return layoutScanner.scan(path); 38 | } 39 | 40 | @Override public boolean equals(Object obj) { 41 | return EqualsBuilder.reflectionEquals(this, obj, false); 42 | } 43 | 44 | @Override public int hashCode() { 45 | return HashCodeBuilder.reflectionHashCode(this, false); 46 | } 47 | 48 | @Override public String toString() { 49 | return ToStringBuilder.reflectionToString(this); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/LayoutScanner.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.FilenameFilter; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | import static de.ito.gradle.plugin.androidstringextractor.internal.Util.assertPathIsDirectory; 10 | 11 | class LayoutScanner { 12 | private static final FilenameFilter XML_FILTER = new XmlFilter(); 13 | 14 | private final XmlFileReader xmlFileReader; 15 | private final LayoutParser layoutParser; 16 | private final XmlFileWriter xmlFileWriter; 17 | private final ReferenceReplacer referenceReplacer; 18 | 19 | LayoutScanner(XmlFileReader xmlFileReader, LayoutParser layoutParser, XmlFileWriter xmlFileWriter, 20 | ReferenceReplacer referenceReplacer) { 21 | this.xmlFileReader = xmlFileReader; 22 | this.layoutParser = layoutParser; 23 | this.xmlFileWriter = xmlFileWriter; 24 | this.referenceReplacer = referenceReplacer; 25 | } 26 | 27 | List scan(File flavorPath) { 28 | File layoutPath = new File(flavorPath, "/res/layout"); 29 | 30 | if(!layoutPath.exists()) return Collections.emptyList(); 31 | 32 | assertPathIsDirectory(layoutPath); 33 | File[] layoutFiles = layoutPath.listFiles(XML_FILTER); 34 | 35 | return createLayoutForEachFile(layoutFiles); 36 | } 37 | 38 | private List createLayoutForEachFile(File[] layoutFiles) { 39 | List layouts = new ArrayList<>(); 40 | 41 | for (File layoutFile : layoutFiles) { 42 | layouts.add(new Layout(layoutFile, xmlFileReader, layoutParser, xmlFileWriter, 43 | referenceReplacer)); 44 | } 45 | 46 | return layouts; 47 | } 48 | 49 | private static class XmlFilter implements FilenameFilter { 50 | @Override public boolean accept(File dir, String name) { 51 | return name.endsWith(".xml"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/LayoutScannerTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import org.junit.Before; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.rules.TemporaryFolder; 11 | 12 | import static org.hamcrest.CoreMatchers.equalTo; 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.mockito.Mockito.mock; 15 | 16 | public class LayoutScannerTest { 17 | @Rule public TemporaryFolder folder = new TemporaryFolder(); 18 | 19 | private LayoutScanner layoutScanner; 20 | private XmlFileReader xmlFileReader; 21 | private LayoutParser layoutParser; 22 | private XmlFileWriter xmlFileWriter; 23 | private ReferenceReplacer referenceReplacer; 24 | 25 | @Before public void setUp() throws Exception { 26 | xmlFileReader = mock(XmlFileReader.class); 27 | layoutParser = mock(LayoutParser.class); 28 | xmlFileWriter = mock(XmlFileWriter.class); 29 | referenceReplacer = mock(ReferenceReplacer.class); 30 | 31 | layoutScanner = 32 | new LayoutScanner(xmlFileReader, layoutParser, xmlFileWriter, referenceReplacer); 33 | } 34 | 35 | @Test public void when_scanFlavor_should_returnLayouts() throws Exception { 36 | List expected = createDummyLayouts(); 37 | 38 | List actual = layoutScanner.scan(folder.getRoot()); 39 | 40 | assertThat(actual, equalTo(expected)); 41 | } 42 | 43 | private List createDummyLayouts() throws IOException { 44 | File layoutFolder = folder.newFolder("res", "layout"); 45 | File layoutFile = new File(layoutFolder, "layout.xml"); 46 | layoutFile.createNewFile(); 47 | 48 | Layout layout = 49 | new Layout(layoutFile, xmlFileReader, layoutParser, xmlFileWriter, referenceReplacer); 50 | 51 | return Collections.singletonList(layout); 52 | } 53 | } -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/FlavorScannerTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import org.junit.Before; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.rules.TemporaryFolder; 11 | 12 | import static org.hamcrest.CoreMatchers.equalTo; 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.mockito.Mockito.mock; 15 | 16 | public class FlavorScannerTest { 17 | @Rule public TemporaryFolder folder = new TemporaryFolder(); 18 | 19 | private StringValuesReader stringValuesReader; 20 | private StringValuesWriter stringValuesWriter; 21 | private LayoutScanner layoutScanner; 22 | 23 | private FlavorScanner flavorScanner; 24 | 25 | @Before public void setUp() throws IOException { 26 | stringValuesReader = mock(StringValuesReader.class); 27 | stringValuesWriter = mock(StringValuesWriter.class); 28 | layoutScanner = mock(LayoutScanner.class); 29 | 30 | flavorScanner = new FlavorScanner(stringValuesReader, stringValuesWriter, layoutScanner); 31 | } 32 | 33 | @Test public void when_scanFolder_should_returnFlavors() throws Exception { 34 | List expected = createDummyFlavors(); 35 | File projectPath = createDummyFlavorsInPath(); 36 | 37 | List actual = flavorScanner.scan(projectPath); 38 | 39 | assertThat(actual, equalTo(expected)); 40 | } 41 | 42 | private List createDummyFlavors() { 43 | return Collections.singletonList( 44 | new Flavor(new File(folder.getRoot(), "app/src/main/"), stringValuesReader, 45 | stringValuesWriter, 46 | layoutScanner)); 47 | } 48 | 49 | private File createDummyFlavorsInPath() { 50 | File temporaryProjectPath = folder.getRoot(); 51 | File flavorPath = new File(temporaryProjectPath, "app/src/main/"); 52 | 53 | flavorPath.mkdirs(); 54 | 55 | return new File(temporaryProjectPath.getPath() + "/app"); 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/FlavorScanner.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.FilenameFilter; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import static de.ito.gradle.plugin.androidstringextractor.internal.Util.assertPathIsDirectory; 10 | 11 | class FlavorScanner { 12 | private static final FilenameFilter DIRECTORY_FILTER = new DirectoryFilter(); 13 | 14 | private final StringValuesReader stringValuesReader; 15 | private final StringValuesWriter stringValuesWriter; 16 | private final LayoutScanner layoutScanner; 17 | 18 | FlavorScanner(StringValuesReader stringValuesReader, StringValuesWriter stringValuesWriter, 19 | LayoutScanner layoutScanner) { 20 | this.stringValuesReader = stringValuesReader; 21 | this.stringValuesWriter = stringValuesWriter; 22 | this.layoutScanner = layoutScanner; 23 | } 24 | 25 | List scan(File projectPath) throws FileNotFoundException { 26 | File flavorPath = new File(projectPath, "/src/"); 27 | 28 | assertPathIsDirectory(flavorPath); 29 | File[] flavorDirectories = listDirectoriesInFlavorPath(flavorPath); 30 | 31 | return createFlavorForEachDirectory(flavorDirectories); 32 | } 33 | 34 | private File[] listDirectoriesInFlavorPath(File flavorPath) { 35 | return flavorPath.listFiles(DIRECTORY_FILTER); 36 | } 37 | 38 | private List createFlavorForEachDirectory(File[] flavorDirectories) { 39 | List flavors = new ArrayList<>(); 40 | 41 | for (File flavorDirectory : flavorDirectories) { 42 | flavors.add( 43 | new Flavor(flavorDirectory, stringValuesReader, stringValuesWriter, layoutScanner)); 44 | } 45 | 46 | return flavors; 47 | } 48 | 49 | private static class DirectoryFilter implements FilenameFilter { 50 | @Override public boolean accept(File dir, String name) { 51 | File directory = new File(dir, name); 52 | 53 | return directory.isDirectory(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/XmlFileWriterTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.charset.Charset; 6 | import java.nio.file.Files; 7 | import java.nio.file.Paths; 8 | import java.util.List; 9 | import javax.xml.parsers.ParserConfigurationException; 10 | import org.junit.Before; 11 | import org.junit.Rule; 12 | import org.junit.Test; 13 | import org.junit.rules.TemporaryFolder; 14 | import org.w3c.dom.Document; 15 | import org.w3c.dom.Element; 16 | import org.w3c.dom.Text; 17 | 18 | import static org.hamcrest.CoreMatchers.equalTo; 19 | import static org.junit.Assert.assertThat; 20 | 21 | public class XmlFileWriterTest { 22 | @Rule public TemporaryFolder folder = new TemporaryFolder(); 23 | 24 | private XmlFileWriter xmlFileWriter; 25 | 26 | @Before 27 | public void setUp() throws Exception { 28 | xmlFileWriter = new XmlFileWriter(); 29 | } 30 | 31 | @Test 32 | public void when_writeDocumentToFile_should_writeFileWithContent() throws Exception { 33 | Document document = dummyDocument(); 34 | File file = new File(folder.getRoot(), "file.xml"); 35 | String expected = 36 | "value"; 37 | 38 | xmlFileWriter.write(document, file); 39 | 40 | assertThat(contentOf(file), equalTo(expected)); 41 | } 42 | 43 | private Document dummyDocument() throws ParserConfigurationException { 44 | Document document = Util.createEmptyDocument(); 45 | Element tag = document.createElement("tag"); 46 | Text value = document.createTextNode("value"); 47 | 48 | tag.appendChild(value); 49 | document.appendChild(tag); 50 | 51 | return document; 52 | } 53 | 54 | private String contentOf(File file) throws IOException { 55 | List content = (Files.readAllLines(Paths.get(file.toURI()), Charset.forName("UTF-8"))); 56 | String contentOf = ""; 57 | for (String line : content) { 58 | contentOf += line; 59 | } 60 | 61 | return contentOf; 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/LayoutStringExtractor.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.List; 6 | import javax.xml.parsers.ParserConfigurationException; 7 | import javax.xml.transform.TransformerException; 8 | import javax.xml.xpath.XPathExpressionException; 9 | import org.xml.sax.SAXException; 10 | 11 | public class LayoutStringExtractor { 12 | private final AndroidProjectFactory factory; 13 | 14 | public LayoutStringExtractor(AndroidProjectFactory factory) { 15 | this.factory = factory; 16 | } 17 | 18 | public void extract(String projectPath) 19 | throws TransformerException, IOException, ParserConfigurationException, SAXException, 20 | XPathExpressionException { 21 | AndroidProject project = factory.create(new File(projectPath)); 22 | List flavors = project.readFlavors(); 23 | for (Flavor flavor : flavors) { 24 | handleFlavor(flavor); 25 | } 26 | } 27 | 28 | private void handleFlavor(Flavor flavor) 29 | throws TransformerException, IOException, ParserConfigurationException, SAXException, 30 | XPathExpressionException { 31 | StringValues stringValues = flavor.readStringValues(); 32 | 33 | List layouts = flavor.readLayouts(); 34 | for (Layout layout : layouts) { 35 | handleLayout(layout, stringValues); 36 | } 37 | 38 | if(stringValues.hasChanged()) flavor.writeStringValues(stringValues); 39 | } 40 | 41 | private void handleLayout(Layout layout, StringValues stringValues) 42 | throws TransformerException, IOException, ParserConfigurationException, SAXException, 43 | XPathExpressionException { 44 | List stringOccurrences = layout.readStrings(); 45 | for (StringOccurrence string : stringOccurrences) { 46 | handleString(string, layout, stringValues); 47 | } 48 | 49 | layout.writeStrings(stringOccurrences); 50 | } 51 | 52 | private void handleString(StringOccurrence occurrence, Layout layout, StringValues stringValues) { 53 | if (occurrence.hasHardCodedValue()) { 54 | String key = layout.computeStringReference(occurrence); 55 | stringValues.put(key, occurrence.getValue()); 56 | occurrence.replaceHardCodedValueByReference(key); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/XmlFileReaderTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.StringWriter; 6 | import java.nio.charset.Charset; 7 | import javax.xml.transform.OutputKeys; 8 | import javax.xml.transform.Transformer; 9 | import javax.xml.transform.TransformerException; 10 | import javax.xml.transform.TransformerFactory; 11 | import javax.xml.transform.dom.DOMSource; 12 | import javax.xml.transform.stream.StreamResult; 13 | import org.gradle.internal.impldep.com.google.common.io.Files; 14 | import org.junit.Before; 15 | import org.junit.Rule; 16 | import org.junit.Test; 17 | import org.junit.rules.TemporaryFolder; 18 | import org.w3c.dom.Document; 19 | 20 | import static org.hamcrest.CoreMatchers.equalTo; 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | 23 | public class XmlFileReaderTest { 24 | @Rule public TemporaryFolder folder = new TemporaryFolder(); 25 | 26 | private XmlFileReader xmlFileReader; 27 | 28 | @Before public void setUp() throws IOException { 29 | this.xmlFileReader = new XmlFileReader(); 30 | } 31 | 32 | @Test 33 | public void when_readDocumentFromFile_should_readFileContent() throws Exception { 34 | String expected = 35 | "value"; 36 | File file = createXmlFileFrom(expected); 37 | 38 | Document actual = xmlFileReader.read(file); 39 | 40 | assertThat(contentOf(actual), equalTo(expected)); 41 | } 42 | 43 | private File createXmlFileFrom(String content) throws IOException { 44 | File file = new File(folder.getRoot(), "file.xml"); 45 | 46 | Files.write(content, file, Charset.forName("UTF-8")); 47 | 48 | return file; 49 | } 50 | 51 | private String contentOf(Document document) throws TransformerException { 52 | DOMSource domSource = new DOMSource(document); 53 | StringWriter writer = new StringWriter(); 54 | StreamResult result = new StreamResult(writer); 55 | 56 | TransformerFactory tf = TransformerFactory.newInstance(); 57 | Transformer transformer = tf.newTransformer(); 58 | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); 59 | transformer.setOutputProperty(OutputKeys.INDENT, "no"); 60 | transformer.transform(domSource, result); 61 | 62 | return writer.toString(); 63 | } 64 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/FlavorTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import static org.hamcrest.CoreMatchers.equalTo; 10 | import static org.hamcrest.MatcherAssert.assertThat; 11 | import static org.mockito.Matchers.any; 12 | import static org.mockito.Matchers.eq; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.when; 16 | 17 | public class FlavorTest { 18 | private StringValuesReader stringValuesReader; 19 | private StringValuesWriter stringValuesWriter; 20 | private LayoutScanner layoutScanner; 21 | private File path; 22 | 23 | private Flavor flavor; 24 | 25 | @Before public void setUp() throws Exception { 26 | stringValuesReader = mock(StringValuesReader.class); 27 | stringValuesWriter = mock(StringValuesWriter.class); 28 | layoutScanner = mock(LayoutScanner.class); 29 | path = mock(File.class); 30 | 31 | flavor = new Flavor(path, stringValuesReader, stringValuesWriter, layoutScanner); 32 | } 33 | 34 | @Test public void when_readStringValues_should_returnStringValues() throws Exception { 35 | StringValues expected = dummyStringValues(); 36 | when(stringValuesReader.read(any(File.class))).thenReturn(expected); 37 | 38 | StringValues actual = 39 | flavor.readStringValues(); 40 | 41 | assertThat(actual, equalTo(expected)); 42 | } 43 | 44 | @Test public void when_writeStringValues_should_writeStringValues() throws Exception { 45 | StringValues stringValues = dummyStringValues(); 46 | 47 | flavor.writeStringValues(stringValues); 48 | 49 | verify(stringValuesWriter).write(eq(stringValues), any(File.class)); 50 | } 51 | 52 | @Test public void when_readLayouts_should_returnLayouts() throws Exception { 53 | List expected = dummyLayouts(); 54 | when(layoutScanner.scan(any(File.class))).thenReturn(expected); 55 | 56 | List actual = flavor.readLayouts(); 57 | 58 | assertThat(actual, equalTo(expected)); 59 | } 60 | 61 | private StringValues dummyStringValues() { 62 | StringValues stringValues = new StringValues(); 63 | 64 | return stringValues; 65 | } 66 | 67 | private List dummyLayouts() { 68 | Layout layout = new Layout(new File("layout.xml"), null, null, null, null); 69 | 70 | return Collections.singletonList(layout); 71 | } 72 | } -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/StringValuesWriterTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import javax.xml.parsers.ParserConfigurationException; 5 | import javax.xml.transform.TransformerException; 6 | import org.hamcrest.BaseMatcher; 7 | import org.hamcrest.Description; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.mockito.Matchers; 11 | import org.w3c.dom.Document; 12 | 13 | import static org.mockito.Matchers.argThat; 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.verify; 16 | 17 | public class StringValuesWriterTest { 18 | 19 | private XmlFileWriter xmlFileWriter; 20 | private StringValuesWriter stringValuesWriter; 21 | 22 | @Before public void setUp() throws Exception { 23 | xmlFileWriter = mock(XmlFileWriter.class); 24 | stringValuesWriter = new StringValuesWriter(xmlFileWriter); 25 | } 26 | 27 | @Test public void when_writeStringValuesToFile_should_writeValuesToFile() throws Exception { 28 | Document expectedDocument = createExpectedDocument(); 29 | StringValues stringValues = StringValuesReaderTest.createDummyStringValues(); 30 | File flavorPath = new File(""); 31 | File stringValuesFile = new File(flavorPath, "res/values/string_layouts.xml"); 32 | 33 | stringValuesWriter.write(stringValues, flavorPath); 34 | 35 | verify(xmlFileWriter).write(eq(expectedDocument), Matchers.eq(stringValuesFile)); 36 | } 37 | 38 | private Document createExpectedDocument() 39 | throws ParserConfigurationException, TransformerException { 40 | Document document = StringValuesReaderTest.createDummyDocument(); 41 | 42 | document.insertBefore(document.createComment(StringValuesWriter.DO_NOT_MODIFY_NOTE), 43 | document.getFirstChild()); 44 | 45 | return document; 46 | } 47 | 48 | private Document eq(Document document) { 49 | return argThat(new DocumentMatch(document)); 50 | } 51 | 52 | private class DocumentMatch extends BaseMatcher { 53 | private final Document expectedDocument; 54 | 55 | private DocumentMatch(Document expectedDocument) { 56 | this.expectedDocument = expectedDocument; 57 | } 58 | 59 | @Override public boolean matches(final Object actualDocument) { 60 | final Document actual = (Document) actualDocument; 61 | return actual.isEqualNode(expectedDocument); 62 | } 63 | 64 | @Override public void describeTo(final Description description) { 65 | description.appendText("Documents do not match."); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/StringValuesWriter.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Map; 6 | import javax.xml.parsers.ParserConfigurationException; 7 | import javax.xml.transform.TransformerException; 8 | import org.w3c.dom.Attr; 9 | import org.w3c.dom.Comment; 10 | import org.w3c.dom.Document; 11 | import org.w3c.dom.Element; 12 | import org.w3c.dom.Text; 13 | 14 | class StringValuesWriter { 15 | static final String DO_NOT_MODIFY_NOTE = 16 | "\tThis file is auto-generated DO NOT insert new strings here manually!!!\n\t\tJust insert the string value" 17 | + " in the respective widget text within the layout file, this value will then be automatically added to this file" 18 | + " with the correct reference\t\n"; 19 | 20 | private XmlFileWriter xmlFileWriter; 21 | 22 | StringValuesWriter(XmlFileWriter xmlFileWriter) { 23 | this.xmlFileWriter = xmlFileWriter; 24 | } 25 | 26 | void write(StringValues stringValues, File flavorPath) 27 | throws ParserConfigurationException, TransformerException, IOException { 28 | Document document = buildStringValuesDocument(stringValues); 29 | File stringValuesFile = new File(flavorPath, "res/values/string_layouts.xml"); 30 | 31 | xmlFileWriter.write(document, stringValuesFile); 32 | } 33 | 34 | private Document buildStringValuesDocument(StringValues stringValues) throws 35 | ParserConfigurationException { 36 | Document stringValuesDocument = Util.createEmptyDocument(); 37 | Comment doNotEditManuallyNote = createNote(stringValuesDocument); 38 | Element resources = stringValuesDocument.createElement("resources"); 39 | 40 | appendStrings(stringValuesDocument, resources, stringValues); 41 | 42 | stringValuesDocument.appendChild(doNotEditManuallyNote); 43 | stringValuesDocument.appendChild(resources); 44 | 45 | return stringValuesDocument; 46 | } 47 | 48 | private Comment createNote(Document stringValuesDocument) { 49 | return stringValuesDocument.createComment(DO_NOT_MODIFY_NOTE); 50 | } 51 | 52 | private void appendStrings(Document document, Element resources, StringValues stringValues) { 53 | Map values = stringValues.getValues(); 54 | for (String key : values.keySet()) { 55 | Element string = buildString(document, key, values.get(key)); 56 | 57 | resources.appendChild(string); 58 | } 59 | } 60 | 61 | private Element buildString(Document document, String key, String stringValue) { 62 | Element string = document.createElement("string"); 63 | 64 | Attr name = document.createAttribute("name"); 65 | name.setValue(key); 66 | string.setAttributeNode(name); 67 | 68 | Text value = document.createTextNode(stringValue); 69 | string.appendChild(value); 70 | 71 | return string; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/LayoutParserTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import javax.xml.parsers.ParserConfigurationException; 6 | import org.junit.Before; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.junit.rules.TemporaryFolder; 10 | import org.w3c.dom.Document; 11 | import org.w3c.dom.Element; 12 | 13 | import static org.hamcrest.CoreMatchers.equalTo; 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | 16 | public class LayoutParserTest { 17 | @Rule public TemporaryFolder folder = new TemporaryFolder(); 18 | 19 | private LayoutParser layoutParser; 20 | 21 | @Before public void setUp() throws Exception { 22 | layoutParser = new LayoutParser(); 23 | } 24 | 25 | @Test public void when_parseDocument_should_returnStringOccurrences() throws Exception { 26 | List expected = createDummyStrings(); 27 | Document document = createDummyDocument(); 28 | 29 | List actual = layoutParser.parse(document); 30 | 31 | assertThat(actual, equalTo(expected)); 32 | } 33 | 34 | private List createDummyStrings() { 35 | StringOccurrence string1 = new StringOccurrence("id1", "text", "text"); 36 | StringOccurrence string2 = new StringOccurrence("id2", "hint", "hint"); 37 | 38 | return Arrays.asList(string1, string2); 39 | } 40 | 41 | private Document createDummyDocument() throws ParserConfigurationException { 42 | Document document = Util.createEmptyDocument(); 43 | 44 | Element linearLayout = document.createElement("LinearLayout"); 45 | linearLayout.setAttribute("xmlns:android", 46 | "http://schemas.android.com/apk/res/android"); 47 | linearLayout.setAttribute("android:orientation", "vertical"); 48 | document.appendChild(linearLayout); 49 | 50 | Element textViewWithIdAndText = document.createElement("TextView"); 51 | textViewWithIdAndText.setAttribute("android:text", "text"); 52 | textViewWithIdAndText.setAttribute("android:id", "@+id/id1"); 53 | linearLayout.appendChild(textViewWithIdAndText); 54 | 55 | Element textViewWithIdAndHint = document.createElement("TextView"); 56 | textViewWithIdAndHint.setAttribute("android:hint", "hint"); 57 | textViewWithIdAndHint.setAttribute("android:id", "@+id/id2"); 58 | linearLayout.appendChild(textViewWithIdAndHint); 59 | 60 | Element textViewWithoutIdAndText = document.createElement("TextView"); 61 | textViewWithoutIdAndText.setAttribute("android:text", "text"); 62 | linearLayout.appendChild(textViewWithoutIdAndText); 63 | 64 | Element textViewWithIdButWithoutTextAndHint = document.createElement("TextView"); 65 | textViewWithIdButWithoutTextAndHint.setAttribute("android:id", "@+id/id3"); 66 | linearLayout.appendChild(textViewWithIdButWithoutTextAndHint); 67 | 68 | return document; 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/Layout.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.List; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | import javax.xml.parsers.ParserConfigurationException; 9 | import javax.xml.transform.TransformerException; 10 | import javax.xml.xpath.XPathExpressionException; 11 | import org.apache.commons.lang3.builder.EqualsBuilder; 12 | import org.apache.commons.lang3.builder.HashCodeBuilder; 13 | import org.apache.commons.lang3.builder.ToStringBuilder; 14 | import org.w3c.dom.Document; 15 | import org.xml.sax.SAXException; 16 | 17 | class Layout { 18 | private final static Pattern FILE_NAME_PATTERN = Pattern.compile("(?\\w+)\\.xml"); 19 | 20 | private final String name; 21 | private final File file; 22 | private final XmlFileReader xmlFileReader; 23 | private final LayoutParser layoutParser; 24 | private final XmlFileWriter xmlFileWriter; 25 | private final ReferenceReplacer referenceReplacer; 26 | 27 | Document document; 28 | 29 | Layout(File file, XmlFileReader xmlFileReader, LayoutParser layoutParser, 30 | XmlFileWriter xmlFileWriter, ReferenceReplacer referenceReplacer) { 31 | this.name = resolveName(file); 32 | this.file = file; 33 | this.layoutParser = layoutParser; 34 | this.xmlFileReader = xmlFileReader; 35 | this.xmlFileWriter = xmlFileWriter; 36 | this.referenceReplacer = referenceReplacer; 37 | } 38 | 39 | String resolveName(File file) { 40 | Matcher matcher = FILE_NAME_PATTERN.matcher(file.getName()); 41 | 42 | if (!matcher.matches()) { 43 | throw new IllegalArgumentException(); 44 | } 45 | 46 | return matcher.group("name"); 47 | } 48 | 49 | List readStrings() 50 | throws ParserConfigurationException, SAXException, IOException { 51 | document = xmlFileReader.read(file); 52 | 53 | return layoutParser.parse(document); 54 | } 55 | 56 | void writeStrings(List strings) 57 | throws TransformerException, IOException, XPathExpressionException { 58 | if (document == null) throw new IllegalStateException("You must read strings first."); 59 | 60 | referenceReplacer.replaceHardCodedValues(document, strings); 61 | 62 | xmlFileWriter.write(document, file); 63 | 64 | document = null; 65 | } 66 | 67 | String computeStringReference(StringOccurrence occurrence) { 68 | String template = "%s_%s_%s"; 69 | return String.format(template, name, occurrence.getId(), occurrence.getAttribute()); 70 | } 71 | 72 | @Override public boolean equals(Object obj) { 73 | return EqualsBuilder.reflectionEquals(this, obj, false); 74 | } 75 | 76 | @Override public int hashCode() { 77 | return HashCodeBuilder.reflectionHashCode(this, false); 78 | } 79 | 80 | @Override public String toString() { 81 | return ToStringBuilder.reflectionToString(this); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/LayoutTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.w3c.dom.Document; 9 | 10 | import static org.hamcrest.CoreMatchers.equalTo; 11 | import static org.hamcrest.CoreMatchers.is; 12 | import static org.hamcrest.CoreMatchers.nullValue; 13 | import static org.junit.Assert.assertThat; 14 | import static org.mockito.Matchers.any; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.verify; 17 | import static org.mockito.Mockito.when; 18 | 19 | public class LayoutTest { 20 | private File file; 21 | private XmlFileReader xmlFileReader; 22 | private LayoutParser layoutParser; 23 | private XmlFileWriter xmlFileWriter; 24 | private ReferenceReplacer referenceReplacer; 25 | 26 | private Layout layout; 27 | 28 | @Before public void setUp() throws Exception { 29 | file = mock(File.class); 30 | xmlFileReader = mock(XmlFileReader.class); 31 | layoutParser = mock(LayoutParser.class); 32 | xmlFileWriter = mock(XmlFileWriter.class); 33 | referenceReplacer = mock(ReferenceReplacer.class); 34 | 35 | when(file.getName()).thenReturn("layout.xml"); 36 | 37 | layout = 38 | new Layout(file, xmlFileReader, layoutParser, xmlFileWriter, referenceReplacer); 39 | } 40 | 41 | @Test public void when_resolveName_should_returnLayoutName() throws Exception { 42 | String expected = "layout"; 43 | File layoutFile = new File(String.format("file://project/res/layout/%s.xml", expected)); 44 | 45 | String actual = layout.resolveName(layoutFile); 46 | 47 | assertThat(actual, equalTo(expected)); 48 | } 49 | 50 | @Test public void when_readStrings_should_returnStringOccurrences() throws Exception { 51 | List expected = dummyStrings(); 52 | when(layoutParser.parse(any(Document.class))).thenReturn(expected); 53 | 54 | List actual = layout.readStrings(); 55 | 56 | assertThat(actual, equalTo(expected)); 57 | } 58 | 59 | @Test public void when_writeStrings_should_writeStrings() throws Exception { 60 | List strings = dummyStrings(); 61 | Document document = mock(Document.class); 62 | layout.document = document; 63 | 64 | layout.writeStrings(strings); 65 | 66 | verify(xmlFileWriter).write(document, file); 67 | assertThat(layout.document, is(nullValue())); 68 | } 69 | 70 | @Test public void when_computeStringReference_should_returnStringReference() throws Exception { 71 | String expected = "layout_id_attribute"; 72 | StringOccurrence stringOccurrence = new StringOccurrence("id", "attribute", "value"); 73 | 74 | String actual = layout.computeStringReference(stringOccurrence); 75 | 76 | assertThat(actual, equalTo(expected)); 77 | } 78 | 79 | private List dummyStrings() { 80 | return Collections.singletonList(dummyString()); 81 | } 82 | 83 | private StringOccurrence dummyString() { 84 | return new StringOccurrence("id", "attribute", "value"); 85 | } 86 | } -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/LayoutParser.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.w3c.dom.Document; 6 | import org.w3c.dom.NamedNodeMap; 7 | import org.w3c.dom.Node; 8 | import org.w3c.dom.NodeList; 9 | 10 | class LayoutParser { 11 | private boolean ignoreMissingId = true; 12 | 13 | void setIgnoreMissingId(boolean ignoreMissingId) { 14 | this.ignoreMissingId = ignoreMissingId; 15 | } 16 | 17 | List parse(Document document) { 18 | List stringOccurrences = new ArrayList<>(); 19 | processNodes(stringOccurrences, document.getChildNodes()); 20 | 21 | return stringOccurrences; 22 | } 23 | 24 | private void processNodes(List stringOccurrences, NodeList nodes) 25 | throws IllegalStateException { 26 | for (int i = 0; i < nodes.getLength(); i++) { 27 | processNode(stringOccurrences, nodes.item(i)); 28 | } 29 | } 30 | 31 | private void processNode(List stringOccurrences, Node node) { 32 | if (node.hasChildNodes()) processNodes(stringOccurrences, node.getChildNodes()); 33 | 34 | if (!node.hasAttributes()) return; 35 | 36 | NamedNodeMap attributes = node.getAttributes(); 37 | Node idAttribute = attributes.getNamedItem("android:id"); 38 | Node textAttribute = attributes.getNamedItem("android:text"); 39 | Node hintAttribute = attributes.getNamedItem("android:hint"); 40 | 41 | if (!ignoreMissingId) validateNode(idAttribute, textAttribute, hintAttribute); 42 | 43 | if (idAttribute == null || (textAttribute == null && hintAttribute == null)) return; 44 | 45 | String id = stripIdPrefix(idAttribute.getNodeValue()); 46 | if (textAttribute != null) { 47 | if (!isDataBinding(textAttribute.getNodeValue())) { 48 | stringOccurrences.add( 49 | new StringOccurrence(id, "text", textAttribute.getNodeValue())); 50 | } 51 | } 52 | if (hintAttribute != null) { 53 | if (!isDataBinding(hintAttribute.getNodeValue())) { 54 | stringOccurrences.add( 55 | new StringOccurrence(id, "hint", hintAttribute.getNodeValue())); 56 | } 57 | } 58 | } 59 | 60 | private void validateNode(Node idAttribute, Node textAttribute, Node hintAttribute) { 61 | if (idAttribute == null || idAttribute.getNodeValue() == null) { 62 | if (textAttribute != null) { 63 | throw new IllegalStateException( 64 | String.format("No id specified for %s", textAttribute.getNodeValue())); 65 | } else { 66 | throw new IllegalStateException( 67 | String.format("No id specified for %s", hintAttribute.getNodeValue())); 68 | } 69 | } 70 | } 71 | 72 | private String stripIdPrefix(String id) { 73 | if (id.startsWith("@+id/")) return id.substring(5); 74 | 75 | return id; 76 | } 77 | 78 | private boolean isDataBinding(String value) { 79 | boolean oneWayDataBinding = value.startsWith("@{"); 80 | boolean twoWayDataBinding = value.startsWith("@={"); 81 | 82 | return oneWayDataBinding || twoWayDataBinding; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/de/ito/gradle/plugin/androidstringextractor/internal/StringValuesReader.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.StringWriter; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | import javax.xml.parsers.ParserConfigurationException; 11 | import javax.xml.transform.OutputKeys; 12 | import javax.xml.transform.Transformer; 13 | import javax.xml.transform.TransformerException; 14 | import javax.xml.transform.TransformerFactory; 15 | import javax.xml.transform.dom.DOMSource; 16 | import javax.xml.transform.stream.StreamResult; 17 | 18 | import org.w3c.dom.Document; 19 | import org.w3c.dom.Node; 20 | import org.w3c.dom.NodeList; 21 | import org.xml.sax.SAXException; 22 | 23 | class StringValuesReader { 24 | private XmlFileReader xmlFileReader; 25 | private Logger logger = Logger.getAnonymousLogger(); 26 | 27 | StringValuesReader(XmlFileReader xmlFileReader) { 28 | this.xmlFileReader = xmlFileReader; 29 | } 30 | 31 | StringValues read(File flavorPath) 32 | throws ParserConfigurationException, SAXException, IOException { 33 | File stringValuesFile = new File(flavorPath, "res/values/string_layouts.xml"); 34 | 35 | return resolveStringValues(stringValuesFile); 36 | } 37 | 38 | private StringValues resolveStringValues(File stringValuesFile) 39 | throws ParserConfigurationException, SAXException, IOException { 40 | if (!stringValuesFile.exists()) return new StringValues(); 41 | 42 | Document document = xmlFileReader.read(stringValuesFile); 43 | NodeList strings = document.getElementsByTagName("string"); 44 | 45 | Map values = resolveStringValues(strings); 46 | 47 | return new StringValues(values); 48 | } 49 | 50 | private Map resolveStringValues(NodeList strings) { 51 | Map values = new LinkedHashMap<>(); 52 | for (int i = 0; i < strings.getLength(); i++) { 53 | try { 54 | handleNode(strings.item(i), values); 55 | }catch(RuntimeException e){ 56 | logNodeHandlingException(strings.item(i), e); 57 | } 58 | } 59 | return values; 60 | } 61 | 62 | private void handleNode(Node stringNode, Map values) { 63 | Node stringNodeNameAttribute = stringNode.getAttributes().getNamedItem("name"); 64 | Node stringNodeValueAttribute = stringNode.getFirstChild(); 65 | if (stringNodeNameAttribute != null && stringNodeValueAttribute != null) { 66 | values.put(stringNodeNameAttribute.getNodeValue(), stringNodeValueAttribute.getNodeValue()); 67 | } 68 | } 69 | 70 | private void logNodeHandlingException(Node node, RuntimeException e) { 71 | try { 72 | logger.log(Level.SEVERE,"an unexpected error occurred while reading string_layouts.\n entry '"+convertNodeToText(node)+"' will not be considered"); 73 | } catch (TransformerException e1) { 74 | logger.log(Level.SEVERE,"an unexpected error occurred while reading string_layouts.\n entry will not be considered"); 75 | } 76 | } 77 | 78 | private String convertNodeToText(Node node )throws TransformerException { 79 | Transformer t = TransformerFactory.newInstance().newTransformer(); 80 | t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); 81 | StringWriter sw = new StringWriter(); 82 | t.transform(new DOMSource(node), new StreamResult(sw)); 83 | return sw.toString(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/LayoutStringExtractorTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import javax.xml.parsers.ParserConfigurationException; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.xml.sax.SAXException; 12 | 13 | import static org.mockito.Matchers.any; 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.when; 17 | 18 | public class LayoutStringExtractorTest { 19 | private MockAndroidProjectFactory factory; 20 | private List expectedStrings; 21 | private StringValues expectedStringValues; 22 | 23 | @Before 24 | public void setUp() throws Exception { 25 | factory = new MockAndroidProjectFactory(); 26 | 27 | expectedStrings = 28 | Arrays.asList(new StringOccurrence("id1", "attr1", "@string/layout1_id1_attr1"), 29 | new StringOccurrence("id2", "attr2", "@string/layout1_id2_attr2")); 30 | 31 | expectedStringValues = new StringValues(); 32 | expectedStringValues.put("layout1_id1_attr1", "referenced string"); 33 | expectedStringValues.put("layout1_id2_attr2", "hardcoded string"); 34 | } 35 | 36 | @Test 37 | public void when_extract_should_modifyLayoutAndStringValues() throws Exception { 38 | LayoutStringExtractor extractor = new LayoutStringExtractor(factory); 39 | 40 | extractor.extract(""); 41 | 42 | verify(factory.layout).writeStrings(expectedStrings); 43 | verify(factory.flavor).writeStringValues(expectedStringValues); 44 | } 45 | 46 | private class MockAndroidProjectFactory extends AndroidProjectFactory { 47 | final Flavor flavor; 48 | final Layout layout; 49 | 50 | MockAndroidProjectFactory() throws IOException, SAXException, ParserConfigurationException { 51 | layoutParser = mock(LayoutParser.class); 52 | xmlFileReader = mock(XmlFileReader.class); 53 | xmlFileWriter = mock(XmlFileWriter.class); 54 | stringValuesReader = mock(StringValuesReader.class); 55 | stringValuesWriter = mock(StringValuesWriter.class); 56 | layoutScanner = mock(LayoutScanner.class); 57 | flavorScanner = mock(FlavorScanner.class); 58 | 59 | flavor = mock(Flavor.class); 60 | List flavors = Collections.singletonList(flavor); 61 | StringValues stringValues = generateActualStringValues(); 62 | layout = mock(Layout.class); 63 | List layouts = Collections.singletonList(layout); 64 | List strings = generateActualStrings(); 65 | 66 | when(flavorScanner.scan(any(File.class))).thenReturn(flavors); 67 | when(flavor.readStringValues()).thenReturn(stringValues); 68 | when(flavor.readLayouts()).thenReturn(layouts); 69 | when(layout.readStrings()).thenReturn(strings); 70 | when(layout.computeStringReference(any(StringOccurrence.class))).thenReturn( 71 | "layout1_id2_attr2"); 72 | } 73 | 74 | private StringValues generateActualStringValues() { 75 | StringValues values = new StringValues(); 76 | values.put("layout1_id1_attr1", "referenced string"); 77 | 78 | return values; 79 | } 80 | 81 | private List generateActualStrings() { 82 | StringOccurrence referencedString = 83 | new StringOccurrence("id1", "attr1", "@string/layout1_id1_attr1"); 84 | StringOccurrence hardcodedString = new StringOccurrence("id2", "attr2", "hardcoded string"); 85 | return Arrays.asList(referencedString, hardcodedString); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/AndroidStringExtractorPluginTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.File; 5 | import java.io.FileWriter; 6 | import java.io.IOException; 7 | import org.gradle.testkit.runner.BuildResult; 8 | import org.gradle.testkit.runner.GradleRunner; 9 | import org.junit.Rule; 10 | import org.junit.Test; 11 | import org.junit.rules.TemporaryFolder; 12 | 13 | import static de.ito.gradle.plugin.androidstringextractor.AndroidStringExtractorPlugin.TASK_NAME; 14 | import static org.gradle.testkit.runner.TaskOutcome.SUCCESS; 15 | import static org.hamcrest.CoreMatchers.equalTo; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | 18 | public class AndroidStringExtractorPluginTest { 19 | @Rule public final TemporaryFolder testProjectDir = new TemporaryFolder(); 20 | 21 | @Test public void test() throws Exception { 22 | setUpTestProject(); 23 | 24 | BuildResult result = GradleRunner.create() 25 | .withProjectDir(testProjectDir.getRoot()) 26 | .withPluginClasspath() 27 | .withArguments(TASK_NAME, "--stacktrace") 28 | .build(); 29 | 30 | assertThat(result.task(":" + TASK_NAME).getOutcome(), equalTo(SUCCESS)); 31 | } 32 | 33 | private void setUpTestProject() throws IOException { 34 | createBuildFile(); 35 | createFlavors(); 36 | } 37 | 38 | private void createBuildFile() throws IOException { 39 | File buildFile = testProjectDir.newFile("build.gradle"); 40 | writeFile(buildFile, "plugins { id 'android-string-extractor' }"); 41 | } 42 | 43 | private void createFlavors() throws IOException { 44 | File flavorPath = new File(testProjectDir.getRoot(), "/src/flavor"); 45 | flavorPath.mkdirs(); 46 | 47 | createFlavor(flavorPath); 48 | 49 | File testPath = new File(testProjectDir.getRoot(), "/src/test"); 50 | testPath.mkdirs(); 51 | } 52 | 53 | private void createFlavor(File flavorPath) throws IOException { 54 | createStringValues(flavorPath); 55 | createLayouts(flavorPath); 56 | } 57 | 58 | private void createStringValues(File flavorPath) throws IOException { 59 | File valuesPath = new File(flavorPath, "/res/values/"); 60 | valuesPath.mkdirs(); 61 | 62 | File stringValuesFile = new File(valuesPath, "string_layouts.xml"); 63 | stringValuesFile.createNewFile(); 64 | 65 | writeFile(stringValuesFile, 66 | "" + "value" + ""); 67 | } 68 | 69 | private void createLayouts(File flavorPath) throws IOException { 70 | File layoutPath = new File(flavorPath, "/res/layout/"); 71 | layoutPath.mkdirs(); 72 | 73 | createLayout(layoutPath); 74 | } 75 | 76 | private void createLayout(File layoutPath) throws IOException { 77 | File layoutFile = new File(layoutPath, "layout.xml"); 78 | 79 | String linearLayout = 80 | "%s\n%s"; 81 | String referencedTextView = 82 | ""; 83 | String hardcodedTextView = 84 | ""; 85 | 86 | writeFile(layoutFile, String.format(linearLayout, referencedTextView, hardcodedTextView)); 87 | } 88 | 89 | private void writeFile(File destination, String content) throws IOException { 90 | BufferedWriter output = null; 91 | try { 92 | output = new BufferedWriter(new FileWriter(destination)); 93 | output.write(content); 94 | } finally { 95 | if (output != null) { 96 | output.close(); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/de/ito/gradle/plugin/androidstringextractor/internal/StringValuesReaderTest.java: -------------------------------------------------------------------------------- 1 | package de.ito.gradle.plugin.androidstringextractor.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | import javax.xml.parsers.ParserConfigurationException; 8 | import org.junit.Before; 9 | import org.junit.Rule; 10 | import org.junit.Test; 11 | import org.junit.rules.TemporaryFolder; 12 | import org.w3c.dom.Attr; 13 | import org.w3c.dom.Document; 14 | import org.w3c.dom.Element; 15 | import org.w3c.dom.Text; 16 | 17 | import static org.hamcrest.CoreMatchers.equalTo; 18 | import static org.junit.Assert.assertThat; 19 | import static org.mockito.Matchers.any; 20 | import static org.mockito.Mockito.mock; 21 | import static org.mockito.Mockito.when; 22 | 23 | public class StringValuesReaderTest { 24 | @Rule public TemporaryFolder folder = new TemporaryFolder(); 25 | 26 | private StringValuesReader stringValuesReader; 27 | private XmlFileReader xmlFileReader; 28 | 29 | @Before public void setUp() throws Exception { 30 | xmlFileReader = mock(XmlFileReader.class); 31 | 32 | stringValuesReader = new StringValuesReader(xmlFileReader); 33 | } 34 | 35 | @Test public void when_readStringValuesFromFile_should_returnStringValues() throws Exception { 36 | StringValues expected = createDummyStringValues(); 37 | Document dummyStringValues = createDummyDocument(); 38 | when(xmlFileReader.read(any(File.class))).thenReturn(dummyStringValues); 39 | File flavorPath = createFileStructure(); 40 | 41 | StringValues actual = stringValuesReader.read(flavorPath); 42 | 43 | assertThat(actual, equalTo(expected)); 44 | } 45 | 46 | @Test 47 | public void given_invalidFormatNode_when_readStringValuesFromFile_should_returnStringValues_andIgnoreInvalidNode() 48 | throws Exception { 49 | StringValues expected = createDummyStringValues(); 50 | Document dummyStringValues = createDummyDocumentWithInvalidNode(); 51 | when(xmlFileReader.read(any(File.class))).thenReturn(dummyStringValues); 52 | File flavorPath = createFileStructure(); 53 | 54 | StringValues actual = stringValuesReader.read(flavorPath); 55 | 56 | assertThat(actual, equalTo(expected)); 57 | } 58 | 59 | 60 | 61 | static StringValues createDummyStringValues() { 62 | Map values = new LinkedHashMap<>(); 63 | 64 | values.put("name", "value"); 65 | 66 | return new StringValues(values); 67 | } 68 | 69 | static Document createDummyDocument() throws ParserConfigurationException { 70 | Document document = Util.createEmptyDocument(); 71 | 72 | Element resources = document.createElement("resources"); 73 | Element string = createStringNodeEntry(document); 74 | 75 | resources.appendChild(string); 76 | document.appendChild(resources); 77 | 78 | return document; 79 | } 80 | 81 | private static Element createStringNodeEntry(Document document) { 82 | Element string = document.createElement("string"); 83 | Attr name = document.createAttribute("name"); 84 | name.setValue("name"); 85 | Text value = document.createTextNode("value"); 86 | 87 | string.setAttributeNode(name); 88 | string.appendChild(value); 89 | return string; 90 | } 91 | 92 | private File createFileStructure() throws IOException { 93 | File flavorPath = new File(folder.getRoot(), "flavor/"); 94 | File stringValuesFile = new File(flavorPath, "res/values/string_layouts.xml"); 95 | stringValuesFile.mkdirs(); 96 | stringValuesFile.createNewFile(); 97 | return flavorPath; 98 | } 99 | 100 | static Document createDummyDocumentWithInvalidNode() throws ParserConfigurationException { 101 | Document document = createDummyDocument(); 102 | 103 | document.getElementsByTagName("resources").item(0).appendChild(createStringNodeEntryWithoutValue(document)); 104 | 105 | return document; 106 | } 107 | 108 | private static Element createStringNodeEntryWithoutValue(Document document) { 109 | Element string = document.createElement("string"); 110 | Attr name = document.createAttribute("name"); 111 | name.setValue("withoutValue"); 112 | 113 | string.setAttributeNode(name); 114 | return string; 115 | } 116 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then 166 | cd "$(dirname "$0")" 167 | fi 168 | 169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 170 | --------------------------------------------------------------------------------