aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java78
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java4
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java4
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoringTest.java57
-rw-r--r--sdk_common/src/com/android/ide/common/resources/ResourceResolver.java43
-rw-r--r--sdk_common/src/com/android/ide/common/resources/ValueResourceParser.java304
-rw-r--r--sdk_common/tests/src/com/android/ide/common/resources/ValueResourceParserTest.java148
9 files changed, 433 insertions, 221 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java
index 62821ae..c77c853 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java
@@ -34,7 +34,7 @@ import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMet
import com.android.SdkConstants;
import com.android.ide.common.rendering.api.ILayoutPullParser;
import com.android.ide.common.rendering.api.IProjectCallback;
-import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.common.resources.ValueResourceParser;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata;
import com.google.common.collect.Maps;
@@ -158,10 +158,8 @@ public class ContextPullParser extends KXmlParser implements ILayoutPullParser {
return VALUE_FILL_PARENT;
}
- // Handle unicode escapes
- if (value != null && value.indexOf('\\') != -1) {
- value = AdtUtils.replaceUnicodeEscapes(value);
- }
+ // Handle unicode escapes etc
+ value = ValueResourceParser.unescapeResourceString(value, false, false);
return value;
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java
index c89a81b..e8e0d79 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java
@@ -36,7 +36,7 @@ import static com.android.SdkConstants.VIEW_INCLUDE;
import com.android.ide.common.rendering.api.ILayoutPullParser;
import com.android.ide.common.rendering.api.ViewInfo;
-import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.common.resources.ValueResourceParser;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.FragmentMenu;
@@ -398,10 +398,8 @@ public class UiElementPullParser extends BasePullParser {
return VALUE_FILL_PARENT;
}
- // Handle unicode escapes
- if (value.indexOf('\\') != -1) {
- value = AdtUtils.replaceUnicodeEscapes(value);
- }
+ // Handle unicode escapes etc
+ value = ValueResourceParser.unescapeResourceString(value, false, false);
return value;
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
index e9d386e..7d0f926 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
@@ -16,12 +16,11 @@
package com.android.ide.eclipse.adt.internal.refactorings.extractstring;
-import static com.android.SdkConstants.AMP_ENTITY;
-import static com.android.SdkConstants.LT_ENTITY;
import static com.android.SdkConstants.QUOT_ENTITY;
import static com.android.SdkConstants.STRING_PREFIX;
import com.android.SdkConstants;
+import com.android.ide.common.resources.ValueResourceParser;
import com.android.ide.common.xml.ManifestData;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
@@ -1218,7 +1217,7 @@ public class ExtractStringRefactoring extends Refactoring {
IStructuredModel smodel = null;
// Single and double quotes must be escaped in the <string>value</string> declaration
- tokenString = escapeString(tokenString);
+ tokenString = ValueResourceParser.escapeResourceString(tokenString);
try {
IStructuredDocument sdoc = null;
@@ -1450,79 +1449,6 @@ public class ExtractStringRefactoring extends Refactoring {
}
/**
- * Escape a string value to be placed in a string resource file such that it complies with
- * the escaping rules described here:
- * http://developer.android.com/guide/topics/resources/string-resource.html
- * More examples of the escaping rules can be found here:
- * http://androidcookbook.com/Recipe.seam?recipeId=2219&recipeFrom=ViewTOC
- * This method assumes that the String is not escaped already.
- *
- * Rules:
- * <ul>
- * <li>Double quotes are needed if string starts or ends with at least one space.
- * <li>{@code @, ?} at beginning of string have to be escaped with a backslash.
- * <li>{@code ', ", \} have to be escaped with a backslash.
- * <li>{@code <, >, &} have to be replaced by their predefined xml entity.
- * <li>{@code \n, \t} have to be replaced by a backslash and the appropriate character.
- * </ul>
- * @param s the string to be escaped
- * @return the escaped string as it would appear in the XML text in a values file
- */
- public static String escapeString(String s) {
- int n = s.length();
- if (n == 0) {
- return "";
- }
-
- StringBuilder sb = new StringBuilder(s.length() * 2);
- boolean hasSpace = s.charAt(0) == ' ' || s.charAt(n - 1) == ' ';
-
- if (hasSpace) {
- sb.append('"');
- } else if (s.charAt(0) == '@' || s.charAt(0) == '?') {
- sb.append('\\');
- }
-
- for (int i = 0; i < n; ++i) {
- char c = s.charAt(i);
- switch (c) {
- case '\'':
- if (!hasSpace) {
- sb.append('\\');
- }
- sb.append(c);
- break;
- case '"':
- case '\\':
- sb.append('\\');
- sb.append(c);
- break;
- case '<':
- sb.append(LT_ENTITY);
- break;
- case '&':
- sb.append(AMP_ENTITY);
- break;
- case '\n':
- sb.append("\\n"); //$NON-NLS-1$
- break;
- case '\t':
- sb.append("\\t"); //$NON-NLS-1$
- break;
- default:
- sb.append(c);
- break;
- }
- }
-
- if (hasSpace) {
- sb.append('"');
- }
-
- return sb.toString();
- }
-
- /**
* Computes the changes to be made to the source Android XML file and
* returns a list of {@link Change}.
* <p/>
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java
index 245d84e..19a7101 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java
@@ -23,6 +23,7 @@ import static org.eclipse.core.resources.IResource.DEPTH_ZERO;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ValueResourceParser;
import com.android.ide.common.xml.ManifestData;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
@@ -34,7 +35,6 @@ import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.project.AndroidNature;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
-import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode;
@@ -1058,7 +1058,7 @@ public class NewProjectCreator {
String value = strings.get(key);
// Escape values if necessary
- value = ExtractStringRefactoring.escapeString(value);
+ value = ValueResourceParser.escapeResourceString(value);
// place them in the template
String stringDef = stringTemplate.replace(PARAM_STRING_NAME, key);
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java
index 7e5866e..ffcfa3e 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java
@@ -15,7 +15,7 @@
*/
package com.android.ide.eclipse.adt.internal.wizards.templates;
-import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring;
+import com.android.ide.common.resources.ValueResourceParser;
import freemarker.template.SimpleScalar;
import freemarker.template.TemplateMethodModel;
@@ -38,6 +38,6 @@ public class FmEscapeXmlStringMethod implements TemplateMethodModel {
throw new TemplateModelException("Wrong arguments");
}
String string = args.get(0).toString();
- return new SimpleScalar(ExtractStringRefactoring.escapeString(string));
+ return new SimpleScalar(ValueResourceParser.escapeResourceString(string));
}
} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoringTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoringTest.java
deleted file mode 100644
index f7d1d4b..0000000
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoringTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Eclipse Public License, Version 1.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.eclipse.org/org/documents/epl-v10.php
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ide.eclipse.adt.internal.refactorings.extractstring;
-
-import junit.framework.TestCase;
-
-public class ExtractStringRefactoringTest extends TestCase {
-
- public void testEscapeStringShouldEscapeXmlSpecialCharacters() throws Exception {
- assertEquals("&lt;", escape("<")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("&amp;", escape("&")); //$NON-NLS-1$ //$NON-NLS-2$
- }
-
- public void testEscapeStringShouldEscapeQuotes() throws Exception {
- assertEquals("\\'", escape("'")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("\\\"", escape("\"")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("\" ' \"", escape(" ' ")); //$NON-NLS-1$ //$NON-NLS-2$
- }
-
- public void testEscapeStringShouldPreserveWhitespace() throws Exception {
- assertEquals("\"at end \"", escape("at end ")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("\" at begin\"", escape(" at begin")); //$NON-NLS-1$ //$NON-NLS-2$
- }
-
- public void testEscapeStringShouldEscapeAtSignAndQuestionMarkOnlyAtBeginning()
- throws Exception {
- assertEquals("\\@text", escape("@text")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("a@text", escape("a@text")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("\\?text", escape("?text")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("a?text", escape("a?text")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("\" ?text\"", escape(" ?text")); //$NON-NLS-1$ //$NON-NLS-2$
- }
-
- public void testEscapeStringShouldEscapeJavaEscapeSequences() throws Exception {
- assertEquals("\\n", escape("\n")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("\\t", escape("\t")); //$NON-NLS-1$ //$NON-NLS-2$
- assertEquals("\\\\", escape("\\")); //$NON-NLS-1$ //$NON-NLS-2$
- }
-
- private static String escape(String unescaped) {
- return ExtractStringRefactoring.escapeString(unescaped);
- }
-}
diff --git a/sdk_common/src/com/android/ide/common/resources/ResourceResolver.java b/sdk_common/src/com/android/ide/common/resources/ResourceResolver.java
index 756ea53..219c93f 100644
--- a/sdk_common/src/com/android/ide/common/resources/ResourceResolver.java
+++ b/sdk_common/src/com/android/ide/common/resources/ResourceResolver.java
@@ -189,7 +189,8 @@ public class ResourceResolver extends RenderResources {
if (reference == null) {
return null;
}
- if (reference.startsWith(PREFIX_THEME_REF)) {
+ if (reference.startsWith(PREFIX_THEME_REF)
+ && reference.length() > PREFIX_THEME_REF.length()) {
// no theme? no need to go further!
if (mTheme == null) {
return null;
@@ -198,6 +199,7 @@ public class ResourceResolver extends RenderResources {
boolean frameworkOnly = false;
// eliminate the prefix from the string
+ String originalReference = reference;
if (reference.startsWith(ANDROID_THEME_PREFIX)) {
frameworkOnly = true;
reference = reference.substring(ANDROID_THEME_PREFIX.length());
@@ -207,7 +209,7 @@ public class ResourceResolver extends RenderResources {
// at this point, value can contain type/name (drawable/foo for instance).
// split it to make sure.
- String[] segments = reference.split("\\/");
+ String[] segments = reference.split("/");
// we look for the referenced item name.
String referenceName = null;
@@ -224,6 +226,18 @@ public class ResourceResolver extends RenderResources {
} else {
// it's just an item name.
referenceName = segments[0];
+
+ // Make sure it looks like a resource name; if not, it could just be a string
+ // which starts with a ?
+ if (!Character.isJavaIdentifierStart(referenceName.charAt(0))) {
+ return null;
+ }
+ for (int i = 1, n = referenceName.length(); i < n; i++) {
+ char c = referenceName.charAt(i);
+ if (!Character.isJavaIdentifierPart(c) && c != '.') {
+ return null;
+ }
+ }
}
// now we look for android: in the referenceName in order to support format
@@ -241,7 +255,7 @@ public class ResourceResolver extends RenderResources {
mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR,
String.format("Couldn't find theme resource %1$s for the current theme",
reference),
- new ResourceValue(ResourceType.ATTR, referenceName, frameworkOnly));
+ new ResourceValue(ResourceType.ATTR, originalReference, frameworkOnly));
}
return item;
@@ -262,16 +276,17 @@ public class ResourceResolver extends RenderResources {
}
// at this point, value contains type/[android:]name (drawable/foo for instance)
- String[] segments = reference.split("\\/");
- if (segments.length <= 1) {
+ String[] segments = reference.split("/");
+ if (segments.length != 2) {
return null;
}
// now we look for android: in the resource name in order to support format
// such as: @drawable/android:name
- if (segments[1].startsWith(PREFIX_ANDROID)) {
+ String referenceName = segments[1];
+ if (referenceName.startsWith(PREFIX_ANDROID)) {
frameworkOnly = true;
- segments[1] = segments[1].substring(PREFIX_ANDROID.length());
+ referenceName = referenceName.substring(PREFIX_ANDROID.length());
}
ResourceType type = ResourceType.getEnum(segments[0]);
@@ -281,7 +296,19 @@ public class ResourceResolver extends RenderResources {
return null;
}
- return findResValue(type, segments[1],
+ // Make sure it looks like a resource name; if not, it could just be a string
+ // which starts with a ?
+ if (!Character.isJavaIdentifierStart(referenceName.charAt(0))) {
+ return null;
+ }
+ for (int i = 1, n = referenceName.length(); i < n; i++) {
+ char c = referenceName.charAt(i);
+ if (!Character.isJavaIdentifierPart(c) && c != '.') {
+ return null;
+ }
+ }
+
+ return findResValue(type, referenceName,
forceFrameworkOnly ? true :frameworkOnly);
}
diff --git a/sdk_common/src/com/android/ide/common/resources/ValueResourceParser.java b/sdk_common/src/com/android/ide/common/resources/ValueResourceParser.java
index aabfd35..aa1f4b8 100644
--- a/sdk_common/src/com/android/ide/common/resources/ValueResourceParser.java
+++ b/sdk_common/src/com/android/ide/common/resources/ValueResourceParser.java
@@ -16,6 +16,11 @@
package com.android.ide.common.resources;
+import static com.android.SdkConstants.AMP_ENTITY;
+import static com.android.SdkConstants.LT_ENTITY;
+
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
import com.android.ide.common.rendering.api.AttrResourceValue;
import com.android.ide.common.rendering.api.DeclareStyleableResourceValue;
import com.android.ide.common.rendering.api.ResourceValue;
@@ -64,7 +69,7 @@ public final class ValueResourceParser extends DefaultHandler {
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (mCurrentValue != null) {
- mCurrentValue.setValue(trimXmlWhitespaces(mCurrentValue.getValue()));
+ mCurrentValue.setValue(unescapeResourceString(mCurrentValue.getValue(), false, true));
}
if (inResources && qName.equals(NODE_RESOURCES)) {
@@ -213,96 +218,263 @@ public final class ValueResourceParser extends DefaultHandler {
}
}
- public static String trimXmlWhitespaces(String value) {
- if (value == null) {
+ /**
+ * Replaces escapes in an XML resource string with the actual characters,
+ * performing unicode substitutions (replacing any {@code \\uNNNN} references in the
+ * given string with the corresponding unicode characters), etc.
+ *
+ *
+ *
+ * @param s the string to unescape
+ * @param escapeEntities XML entities
+ * @param trim whether surrounding space and quotes should be trimmed
+ * @return the string with the escape characters removed and expanded
+ */
+ @Nullable
+ public static String unescapeResourceString(
+ @Nullable String s,
+ boolean escapeEntities, boolean trim) {
+ if (s == null) {
return null;
}
- // look for carriage return and replace all whitespace around it by just 1 space.
- int index;
-
- while ((index = value.indexOf('\n')) != -1) {
- // look for whitespace on each side
- int left = index - 1;
- while (left >= 0) {
- if (Character.isWhitespace(value.charAt(left))) {
- left--;
- } else {
+ // Trim space surrounding optional quotes
+ int i = 0;
+ int n = s.length();
+ if (trim) {
+ while (i < n) {
+ char c = s.charAt(i);
+ if (!Character.isWhitespace(c)) {
+ break;
+ }
+ i++;
+ }
+ while (n > i) {
+ char c = s.charAt(n - 1);
+ if (!Character.isWhitespace(c)) {
+ //See if this was a \, and if so, see whether it was escaped
+ if (n < s.length() && isEscaped(s, n)) {
+ n++;
+ }
break;
}
+ n--;
}
- int right = index + 1;
- int count = value.length();
- while (right < count) {
- if (Character.isWhitespace(value.charAt(right))) {
- right++;
- } else {
+ // Trim surrounding quotes. Note that there can be *any* number of these, and
+ // the left side and right side do not have to match; e.g. you can have
+ // """"f"" => f
+ int quoteStart = i;
+ int quoteEnd = n;
+ while (i < n) {
+ char c = s.charAt(i);
+ if (c != '"') {
+ break;
+ }
+ i++;
+ }
+ // Searching backwards is slightly more complicated; make sure we don't trim
+ // quotes that have been escaped.
+ while (n > i) {
+ char c = s.charAt(n - 1);
+ if (c != '"') {
+ if (n < s.length() && isEscaped(s, n)) {
+ n++;
+ }
break;
}
+ n--;
+ }
+ if (n == i) {
+ return ""; //$NON-NLS-1$
}
- // remove all between left and right (non inclusive) and replace by a single space.
- String leftString = null;
- if (left >= 0) {
- leftString = value.substring(0, left + 1);
+ // Only trim leading spaces if we didn't already process a leading quote:
+ if (i == quoteStart) {
+ while (i < n) {
+ char c = s.charAt(i);
+ if (!Character.isWhitespace(c)) {
+ break;
+ }
+ i++;
+ }
+ }
+ // Only trim trailing spaces if we didn't already process a trailing quote:
+ if (n == quoteEnd) {
+ while (n > i) {
+ char c = s.charAt(n - 1);
+ if (!Character.isWhitespace(c)) {
+ //See if this was a \, and if so, see whether it was escaped
+ if (n < s.length() && isEscaped(s, n)) {
+ n++;
+ }
+ break;
+ }
+ n--;
+ }
}
- String rightString = null;
- if (right < count) {
- rightString = value.substring(right);
+ if (n == i) {
+ return ""; //$NON-NLS-1$
}
+ }
+
+ // If no surrounding whitespace and no escape characters, no need to do any
+ // more work
+ if (i == 0 && n == s.length() && s.indexOf('\\') == -1
+ && (!escapeEntities || s.indexOf('&') == -1)) {
+ return s;
+ }
- if (leftString != null) {
- value = leftString;
- if (rightString != null) {
- value += " " + rightString;
+ StringBuilder sb = new StringBuilder(n - i);
+ for (; i < n; i++) {
+ char c = s.charAt(i);
+ if (c == '\\' && i < n - 1) {
+ char next = s.charAt(i + 1);
+ // Unicode escapes
+ if (next == 'u' && i < n - 5) { // case sensitive
+ String hex = s.substring(i + 2, i + 6);
+ try {
+ int unicodeValue = Integer.parseInt(hex, 16);
+ sb.append((char) unicodeValue);
+ i += 5;
+ continue;
+ } catch (NumberFormatException e) {
+ // Invalid escape: Just proceed to literally transcribe it
+ sb.append(c);
+ }
+ } else if (next == 'n') {
+ sb.append('\n');
+ i++;
+ } else if (next == 't') {
+ sb.append('\t');
+ i++;
+ } else {
+ sb.append(next);
+ i++;
+ continue;
}
} else {
- value = rightString != null ? rightString : "";
+ if (c == '&' && escapeEntities) {
+ if (s.regionMatches(true, i, LT_ENTITY, 0, LT_ENTITY.length())) {
+ sb.append('<');
+ i += LT_ENTITY.length() - 1;
+ continue;
+ } else if (s.regionMatches(true, i, AMP_ENTITY, 0, AMP_ENTITY.length())) {
+ sb.append('&');
+ i += AMP_ENTITY.length() - 1;
+ continue;
+ }
+ }
+ sb.append(c);
}
}
+ s = sb.toString();
- // now we un-escape the string
- int length = value.length();
- char[] buffer = value.toCharArray();
+ return s;
+ }
- for (int i = 0 ; i < length ; i++) {
- if (buffer[i] == '\\' && i + 1 < length) {
- if (buffer[i+1] == 'u') {
- if (i + 5 < length) {
- // this is unicode char \u1234
- int unicodeChar = Integer.parseInt(new String(buffer, i+2, 4), 16);
+ /**
+ * Returns true if the character at the given offset in the string is escaped
+ * (the previous character is a \, and that character isn't itself an escaped \)
+ *
+ * @param s the string
+ * @param index the index of the character in the string to check
+ * @return true if the character is escaped
+ */
+ @VisibleForTesting
+ static boolean isEscaped(String s, int index) {
+ if (index == 0 || index == s.length()) {
+ return false;
+ }
+ int prevPos = index - 1;
+ char prev = s.charAt(prevPos);
+ if (prev != '\\') {
+ return false;
+ }
+ // The character *may* be escaped; not sure if the \ we ran into is
+ // an escape character, or an escaped backslash; we have to search backwards
+ // to be certain.
+ int j = prevPos - 1;
+ while (j >= 0) {
+ if (s.charAt(j) != '\\') {
+ break;
+ }
+ j--;
+ }
+ // If we passed an odd number of \'s, the space is escaped
+ return (prevPos - j) % 2 == 1;
+ }
- // put the unicode char at the location of the \
- buffer[i] = (char)unicodeChar;
+ /**
+ * Escape a string value to be placed in a string resource file such that it complies with
+ * the escaping rules described here:
+ * http://developer.android.com/guide/topics/resources/string-resource.html
+ * More examples of the escaping rules can be found here:
+ * http://androidcookbook.com/Recipe.seam?recipeId=2219&recipeFrom=ViewTOC
+ * This method assumes that the String is not escaped already.
+ *
+ * Rules:
+ * <ul>
+ * <li>Double quotes are needed if string starts or ends with at least one space.
+ * <li>{@code @, ?} at beginning of string have to be escaped with a backslash.
+ * <li>{@code ', ", \} have to be escaped with a backslash.
+ * <li>{@code <, >, &} have to be replaced by their predefined xml entity.
+ * <li>{@code \n, \t} have to be replaced by a backslash and the appropriate character.
+ * </ul>
+ * @param s the string to be escaped
+ * @return the escaped string as it would appear in the XML text in a values file
+ */
+ public static String escapeResourceString(String s) {
+ int n = s.length();
+ if (n == 0) {
+ return "";
+ }
- // offset the rest of the buffer since we go from 6 to 1 char
- if (i + 6 < buffer.length) {
- System.arraycopy(buffer, i+6, buffer, i+1, length - i - 6);
- }
- length -= 5;
- }
- } else {
- if (buffer[i+1] == 'n') {
- // replace the 'n' char with \n
- buffer[i+1] = '\n';
- }
+ StringBuilder sb = new StringBuilder(s.length() * 2);
+ boolean hasSpace = s.charAt(0) == ' ' || s.charAt(n - 1) == ' ';
- // offset the buffer to erase the \
- System.arraycopy(buffer, i+1, buffer, i, length - i - 1);
- length--;
- }
- } else if (buffer[i] == '"') {
- // if the " was escaped it would have been processed above.
- // offset the buffer to erase the "
- System.arraycopy(buffer, i+1, buffer, i, length - i - 1);
- length--;
-
- // unlike when unescaping, we want to process the next char too
- i--;
+ if (hasSpace) {
+ sb.append('"');
+ } else if (s.charAt(0) == '@' || s.charAt(0) == '?') {
+ sb.append('\\');
+ }
+
+ for (int i = 0; i < n; ++i) {
+ char c = s.charAt(i);
+ switch (c) {
+ case '\'':
+ if (!hasSpace) {
+ sb.append('\\');
+ }
+ sb.append(c);
+ break;
+ case '"':
+ case '\\':
+ sb.append('\\');
+ sb.append(c);
+ break;
+ case '<':
+ sb.append(LT_ENTITY);
+ break;
+ case '&':
+ sb.append(AMP_ENTITY);
+ break;
+ case '\n':
+ sb.append("\\n"); //$NON-NLS-1$
+ break;
+ case '\t':
+ sb.append("\\t"); //$NON-NLS-1$
+ break;
+ default:
+ sb.append(c);
+ break;
}
}
- return new String(buffer, 0, length);
+ if (hasSpace) {
+ sb.append('"');
+ }
+
+ return sb.toString();
}
}
diff --git a/sdk_common/tests/src/com/android/ide/common/resources/ValueResourceParserTest.java b/sdk_common/tests/src/com/android/ide/common/resources/ValueResourceParserTest.java
new file mode 100644
index 0000000..aed6060
--- /dev/null
+++ b/sdk_common/tests/src/com/android/ide/common/resources/ValueResourceParserTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.common.resources;
+
+import static com.android.ide.common.resources.ValueResourceParser.escapeResourceString;
+import static com.android.ide.common.resources.ValueResourceParser.isEscaped;
+import static com.android.ide.common.resources.ValueResourceParser.unescapeResourceString;
+
+import junit.framework.TestCase;
+
+public class ValueResourceParserTest extends TestCase {
+
+ public void testEscapeStringShouldEscapeXmlSpecialCharacters() throws Exception {
+ assertEquals("&lt;", escapeResourceString("<"));
+ assertEquals("&amp;", escapeResourceString("&"));
+ }
+
+ public void testEscapeStringShouldEscapeQuotes() throws Exception {
+ assertEquals("\\'", escapeResourceString("'"));
+ assertEquals("\\\"", escapeResourceString("\""));
+ assertEquals("\" ' \"", escapeResourceString(" ' "));
+ }
+
+ public void testEscapeStringShouldPreserveWhitespace() throws Exception {
+ assertEquals("\"at end \"", escapeResourceString("at end "));
+ assertEquals("\" at begin\"", escapeResourceString(" at begin"));
+ }
+
+ public void testEscapeStringShouldEscapeAtSignAndQuestionMarkOnlyAtBeginning()
+ throws Exception {
+ assertEquals("\\@text", escapeResourceString("@text"));
+ assertEquals("a@text", escapeResourceString("a@text"));
+ assertEquals("\\?text", escapeResourceString("?text"));
+ assertEquals("a?text", escapeResourceString("a?text"));
+ assertEquals("\" ?text\"", escapeResourceString(" ?text"));
+ }
+
+ public void testEscapeStringShouldEscapeJavaEscapeSequences() throws Exception {
+ assertEquals("\\n", escapeResourceString("\n"));
+ assertEquals("\\t", escapeResourceString("\t"));
+ assertEquals("\\\\", escapeResourceString("\\"));
+ }
+
+ public void testTrim() throws Exception {
+ assertEquals("", unescapeResourceString("", false, true));
+ assertEquals("", unescapeResourceString(" \n ", false, true));
+ assertEquals("test", unescapeResourceString(" test ", false, true));
+ assertEquals(" test ", unescapeResourceString("\" test \"", false, true));
+ assertEquals("test", unescapeResourceString("\n\t test \t\n ", false, true));
+
+ assertEquals("test\n", unescapeResourceString(" test\\n ", false, true));
+ assertEquals(" test\n ", unescapeResourceString("\" test\\n \"", false, true));
+ assertEquals("te\\st", unescapeResourceString("\n\t te\\\\st \t\n ", false, true));
+ assertEquals("te\\st", unescapeResourceString(" te\\\\st ", false, true));
+ assertEquals("test", unescapeResourceString("\"\"\"test\"\" ", false, true));
+ assertEquals("\"test\"", unescapeResourceString("\"\"\\\"test\\\"\" ", false, true));
+ assertEquals("test ", unescapeResourceString("test\\ ", false, true));
+ assertEquals("\\\\\\", unescapeResourceString("\\\\\\\\\\\\ ", false, true));
+ assertEquals("\\\\\\ ", unescapeResourceString("\\\\\\\\\\\\\\ ", false, true));
+ }
+
+ public void testNoTrim() throws Exception {
+ assertEquals("", unescapeResourceString("", false, false));
+ assertEquals(" \n ", unescapeResourceString(" \n ", false, false));
+ assertEquals(" test ", unescapeResourceString(" test ", false, false));
+ assertEquals("\" test \"", unescapeResourceString("\" test \"", false, false));
+ assertEquals("\n\t test \t\n ", unescapeResourceString("\n\t test \t\n ", false, false));
+
+ assertEquals(" test\n ", unescapeResourceString(" test\\n ", false, false));
+ assertEquals("\" test\n \"", unescapeResourceString("\" test\\n \"", false, false));
+ assertEquals("\n\t te\\st \t\n ", unescapeResourceString("\n\t te\\\\st \t\n ", false, false));
+ assertEquals(" te\\st ", unescapeResourceString(" te\\\\st ", false, false));
+ assertEquals("\"\"\"test\"\" ", unescapeResourceString("\"\"\"test\"\" ", false, false));
+ assertEquals("\"\"\"test\"\" ", unescapeResourceString("\"\"\\\"test\\\"\" ", false, false));
+ assertEquals("test ", unescapeResourceString("test\\ ", false, false));
+ assertEquals("\\\\\\ ", unescapeResourceString("\\\\\\\\\\\\ ", false, false));
+ assertEquals("\\\\\\ ", unescapeResourceString("\\\\\\\\\\\\\\ ", false, false));
+ }
+
+ public void testUnescapeStringShouldUnescapeXmlSpecialCharacters() throws Exception {
+ assertEquals("&lt;", unescapeResourceString("&lt;", false, true));
+ assertEquals("<", unescapeResourceString("&lt;", true, true));
+ assertEquals("<", unescapeResourceString(" &lt; ", true, true));
+ assertEquals("&amp;", unescapeResourceString("&amp;", false, true));
+ assertEquals("&", unescapeResourceString("&amp;", true, true));
+ assertEquals("&", unescapeResourceString(" &amp; ", true, true));
+ assertEquals("!<", unescapeResourceString("!&lt;", true, true));
+ }
+
+ public void testUnescapeStringShouldUnescapeQuotes() throws Exception {
+ assertEquals("'", unescapeResourceString("\\'", false, true));
+ assertEquals("\"", unescapeResourceString("\\\"", false, true));
+ assertEquals(" ' ", unescapeResourceString("\" ' \"", false, true));
+ }
+
+ public void testUnescapeStringShouldPreserveWhitespace() throws Exception {
+ assertEquals("at end ", unescapeResourceString("\"at end \"", false, true));
+ assertEquals(" at begin", unescapeResourceString("\" at begin\"", false, true));
+ }
+
+ public void testUnescapeStringShouldUnescapeAtSignAndQuestionMarkOnlyAtBeginning()
+ throws Exception {
+ assertEquals("@text", unescapeResourceString("\\@text", false, true));
+ assertEquals("a@text", unescapeResourceString("a@text", false, true));
+ assertEquals("?text", unescapeResourceString("\\?text", false, true));
+ assertEquals("a?text", unescapeResourceString("a?text", false, true));
+ assertEquals(" ?text", unescapeResourceString("\" ?text\"", false, true));
+ }
+
+ public void testUnescapeStringShouldUnescapeJavaUnescapeSequences() throws Exception {
+ assertEquals("\n", unescapeResourceString("\\n", false, true));
+ assertEquals("\t", unescapeResourceString("\\t", false, true));
+ assertEquals("\\", unescapeResourceString("\\\\", false, true));
+ }
+
+ public void testIsEscaped() throws Exception {
+ assertFalse(isEscaped("", 0));
+ assertFalse(isEscaped(" ", 0));
+ assertFalse(isEscaped(" ", 1));
+ assertFalse(isEscaped("x\\y ", 0));
+ assertFalse(isEscaped("x\\y ", 1));
+ assertTrue(isEscaped("x\\y ", 2));
+ assertFalse(isEscaped("x\\y ", 3));
+ assertFalse(isEscaped("x\\\\y ", 0));
+ assertFalse(isEscaped("x\\\\y ", 1));
+ assertTrue(isEscaped("x\\\\y ", 2));
+ assertFalse(isEscaped("x\\\\y ", 3));
+ assertFalse(isEscaped("\\\\\\\\y ", 0));
+ assertTrue(isEscaped( "\\\\\\\\y ", 1));
+ assertFalse(isEscaped("\\\\\\\\y ", 2));
+ assertTrue(isEscaped( "\\\\\\\\y ", 3));
+ assertFalse(isEscaped("\\\\\\\\y ", 4));
+ }
+}