/*
* 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.res2;
import com.android.ide.common.rendering.api.AttrResourceValue;
import com.android.ide.common.rendering.api.ItemResourceValue;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.testutils.TestUtils;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
public class ResourceRepositoryTest extends BaseTestCase {
public void testMergeByCount() throws Exception {
ResourceRepository repo = getResourceRepository();
Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();
assertEquals(6, items.get(ResourceType.DRAWABLE).size());
assertEquals(1, items.get(ResourceType.RAW).size());
assertEquals(4, items.get(ResourceType.LAYOUT).size());
assertEquals(1, items.get(ResourceType.COLOR).size());
assertEquals(4, items.get(ResourceType.STRING).size());
assertEquals(1, items.get(ResourceType.STYLE).size());
assertEquals(1, items.get(ResourceType.ARRAY).size());
assertEquals(7, items.get(ResourceType.ATTR).size());
assertEquals(1, items.get(ResourceType.DECLARE_STYLEABLE).size());
assertEquals(3, items.get(ResourceType.DIMEN).size());
assertEquals(1, items.get(ResourceType.ID).size());
assertEquals(1, items.get(ResourceType.INTEGER).size());
}
public void testMergedResourcesByName() throws Exception {
ResourceRepository repo = getResourceRepository();
// use ? between type and qualifier because of declare-styleable
verifyResourceExists(repo,
"drawable/icon",
"drawable?ldpi/icon",
"drawable/icon2",
"drawable/patch",
"drawable/color_drawable",
"drawable/drawable_ref",
"raw/foo",
"layout/main",
"layout/layout_ref",
"layout/alias_replaced_by_file",
"layout/file_replaced_by_alias",
"color/color",
"string/basic_string",
"string/xliff_string",
"string/styled_string",
"style/style",
"array/string_array",
"attr/dimen_attr",
"attr/string_attr",
"attr/enum_attr",
"attr/flag_attr",
"attr/blah",
"attr/blah2",
"attr/flagAttr",
"declare-styleable/declare_styleable",
"dimen/dimen",
"dimen?sw600dp/offset",
"dimen?sw600dp-v13/offset",
"id/item_id",
"integer/integer"
);
}
public void testBaseStringValue() throws Exception {
ResourceRepository repo = getResourceRepository();
List<ResourceItem> itemList = repo.getResourceItem(ResourceType.STRING, "basic_string");
assertNotNull(itemList);
assertEquals(1, itemList.size());
ResourceValue value = itemList.get(0).getResourceValue(false);
assertNotNull(value);
assertEquals("overlay_string", value.getValue());
}
public void testBaseStyledStringValue() throws Exception {
ResourceRepository repo = getResourceRepository();
List<ResourceItem> itemList = repo.getResourceItem(ResourceType.STRING, "styled_string");
assertNotNull(itemList);
assertEquals(1, itemList.size());
ResourceValue value = itemList.get(0).getResourceValue(false);
assertNotNull(value);
assertEquals("Forgot your username or password?\nVisit google.com/accounts/recovery.",
value.getValue());
}
public void testBaseColorValue() throws Exception {
ResourceRepository repo = getResourceRepository();
List<ResourceItem> itemList = repo.getResourceItem(ResourceType.COLOR, "color");
assertNotNull(itemList);
assertEquals(1, itemList.size());
ResourceValue value = itemList.get(0).getResourceValue(false);
assertNotNull(value);
assertEquals("#FFFFFFFF", value.getValue());
}
public void testBaseLayoutAliasValue() throws Exception {
ResourceRepository repo = getResourceRepository();
List<ResourceItem> itemList = repo.getResourceItem(ResourceType.LAYOUT, "layout_ref");
assertNotNull(itemList);
assertEquals(1, itemList.size());
ResourceValue value = itemList.get(0).getResourceValue(false);
assertNotNull(value);
assertEquals("@layout/ref", value.getValue());
}
public void testBaseAttrValue() throws Exception {
ResourceRepository repo = getResourceRepository();
List<ResourceItem> itemList = repo.getResourceItem(ResourceType.ATTR, "flag_attr");
assertNotNull(itemList);
assertEquals(1, itemList.size());
ResourceValue value = itemList.get(0).getResourceValue(false);
assertNotNull(value);
assertTrue(value instanceof AttrResourceValue);
AttrResourceValue attrValue = (AttrResourceValue) value;
Map<String, Integer> attrValues = attrValue.getAttributeValues();
assertNotNull(attrValues);
assertEquals(3, attrValues.size());
Integer i = attrValues.get("normal");
assertNotNull(i);
assertEquals(Integer.valueOf(0), i);
i = attrValues.get("bold");
assertNotNull(i);
assertEquals(Integer.valueOf(1), i);
i = attrValues.get("italic");
assertNotNull(i);
assertEquals(Integer.valueOf(2), i);
}
public void testBaseStyleValue() throws Exception {
ResourceRepository repo = getResourceRepository();
List<ResourceItem> itemList = repo.getResourceItem(ResourceType.STYLE, "style");
assertNotNull(itemList);
assertEquals(1, itemList.size());
ResourceValue value = itemList.get(0).getResourceValue(false);
assertNotNull(value);
assertTrue(value instanceof StyleResourceValue);
StyleResourceValue styleResourceValue = (StyleResourceValue) value;
assertEquals("@android:style/Holo.Light", styleResourceValue.getParentStyle());
ItemResourceValue styleValue = styleResourceValue.getItem("singleLine", true /*framework*/);
assertNotNull(styleValue);
assertEquals("true", styleValue.getValue());
styleValue = styleResourceValue.getItem("textAppearance", true /*framework*/);
assertNotNull(styleValue);
assertEquals("@style/TextAppearance.WindowTitle", styleValue.getValue());
styleValue = styleResourceValue.getItem("shadowColor", true /*framework*/);
assertNotNull(styleValue);
assertEquals("#BB000000", styleValue.getValue());
styleValue = styleResourceValue.getItem("shadowRadius", true /*framework*/);
assertNotNull(styleValue);
assertEquals("2.75", styleValue.getValue());
styleValue = styleResourceValue.getItem("foo", false /*framework*/);
assertNotNull(styleValue);
assertEquals("foo", styleValue.getValue());
}
public void testUpdateWithBasicFiles() throws Exception {
File root = getIncMergeRoot("basicFiles");
File fakeRoot = getMergedBlobFolder(root);
ResourceMerger resourceMerger = new ResourceMerger();
resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/);
checkSourceFolders(resourceMerger);
List<ResourceSet> sets = resourceMerger.getDataSets();
assertEquals(2, sets.size());
// write the content in a repo.
ResourceRepository repo = new ResourceRepository(false);
resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
// checks the initial state of the repo
Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();
ListMultimap<String, ResourceItem> drawables = items.get(ResourceType.DRAWABLE);
assertNotNull("Drawable null check", drawables);
assertEquals("Drawable size check", 6, drawables.size());
verifyResourceExists(repo,
"drawable/new_overlay",
"drawable/removed",
"drawable?ldpi/removed",
"drawable/touched",
"drawable/removed_overlay",
"drawable/untouched");
// Apply updates
RecordingLogger logger = new RecordingLogger();
// ----------------
// first set is the main one, no change here
ResourceSet mainSet = sets.get(0);
File mainBase = new File(root, "main");
File mainDrawable = new File(mainBase, "drawable");
File mainDrawableLdpi = new File(mainBase, "drawable-ldpi");
// touched/removed files:
File mainDrawableTouched = new File(mainDrawable, "touched.png");
mainSet.updateWith(mainBase, mainDrawableTouched, FileStatus.CHANGED, logger);
checkLogger(logger);
File mainDrawableRemoved = new File(mainDrawable, "removed.png");
mainSet.updateWith(mainBase, mainDrawableRemoved, FileStatus.REMOVED, logger);
checkLogger(logger);
File mainDrawableLdpiRemoved = new File(mainDrawableLdpi, "removed.png");
mainSet.updateWith(mainBase, mainDrawableLdpiRemoved, FileStatus.REMOVED, logger);
checkLogger(logger);
// ----------------
// second set is the overlay one
ResourceSet overlaySet = sets.get(1);
File overlayBase = new File(root, "overlay");
File overlayDrawable = new File(overlayBase, "drawable");
File overlayDrawableHdpi = new File(overlayBase, "drawable-hdpi");
// new/removed files:
File overlayDrawableNewOverlay = new File(overlayDrawable, "new_overlay.png");
overlaySet.updateWith(overlayBase, overlayDrawableNewOverlay, FileStatus.NEW, logger);
checkLogger(logger);
File overlayDrawableRemovedOverlay = new File(overlayDrawable, "removed_overlay.png");
overlaySet.updateWith(overlayBase, overlayDrawableRemovedOverlay, FileStatus.REMOVED,
logger);
checkLogger(logger);
File overlayDrawableHdpiNewAlternate = new File(overlayDrawableHdpi, "new_alternate.png");
overlaySet.updateWith(overlayBase, overlayDrawableHdpiNewAlternate, FileStatus.NEW, logger);
checkLogger(logger);
// validate for duplicates
resourceMerger.validateDataSets();
// check the new content.
resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
drawables = items.get(ResourceType.DRAWABLE);
assertNotNull("Drawable null check", drawables);
assertEquals("Drawable size check", 5, drawables.size());
verifyResourceExists(repo,
"drawable/new_overlay",
"drawable/touched",
"drawable/removed_overlay",
"drawable/untouched",
"drawable?hdpi/new_alternate");
checkRemovedItems(resourceMerger);
}
public void testUpdateWithBasicValues() throws Exception {
File root = getIncMergeRoot("basicValues");
File fakeRoot = getMergedBlobFolder(root);
ResourceMerger resourceMerger = new ResourceMerger();
resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/);
checkSourceFolders(resourceMerger);
List<ResourceSet> sets = resourceMerger.getDataSets();
assertEquals(2, sets.size());
// write the content in a repo.
ResourceRepository repo = new ResourceRepository(false);
resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
// checks the initial state of the repo
Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();
ListMultimap<String, ResourceItem> strings = items.get(ResourceType.STRING);
assertNotNull("String null check", strings);
assertEquals("String size check", 5, strings.size());
verifyResourceExists(repo,
"string/untouched",
"string/touched",
"string/removed",
"string?en/removed",
"string/new_overlay");
// apply updates
RecordingLogger logger = new RecordingLogger();
// ----------------
// first set is the main one, no change here
ResourceSet mainSet = sets.get(0);
File mainBase = new File(root, "main");
File mainValues = new File(mainBase, "values");
File mainValuesEn = new File(mainBase, "values-en");
// touched file:
File mainValuesTouched = new File(mainValues, "values.xml");
mainSet.updateWith(mainBase, mainValuesTouched, FileStatus.CHANGED, logger);
checkLogger(logger);
// removed files
File mainValuesEnRemoved = new File(mainValuesEn, "values.xml");
mainSet.updateWith(mainBase, mainValuesEnRemoved, FileStatus.REMOVED, logger);
checkLogger(logger);
// ----------------
// second set is the overlay one
ResourceSet overlaySet = sets.get(1);
File overlayBase = new File(root, "overlay");
File overlayValues = new File(overlayBase, "values");
File overlayValuesFr = new File(overlayBase, "values-fr");
// new files:
File overlayValuesNew = new File(overlayValues, "values.xml");
overlaySet.updateWith(overlayBase, overlayValuesNew, FileStatus.NEW, logger);
checkLogger(logger);
File overlayValuesFrNew = new File(overlayValuesFr, "values.xml");
overlaySet.updateWith(overlayBase, overlayValuesFrNew, FileStatus.NEW, logger);
checkLogger(logger);
// validate for duplicates
resourceMerger.validateDataSets();
// check the new content.
resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
strings = items.get(ResourceType.STRING);
assertNotNull("String null check", strings);
assertEquals("String size check", 4, strings.size());
verifyResourceExists(repo,
"string/untouched",
"string/touched",
"string/new_overlay",
"string?fr/new_alternate");
checkRemovedItems(resourceMerger);
}
public void testUpdateWithBasicValues2() throws Exception {
File root = getIncMergeRoot("basicValues2");
File fakeRoot = getMergedBlobFolder(root);
ResourceMerger resourceMerger = new ResourceMerger();
resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/);
checkSourceFolders(resourceMerger);
List<ResourceSet> sets = resourceMerger.getDataSets();
assertEquals(2, sets.size());
// write the content in a repo.
ResourceRepository repo = new ResourceRepository(false);
resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
// checks the initial state of the repo
Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();
ListMultimap<String, ResourceItem> strings = items.get(ResourceType.STRING);
assertNotNull("String null check", strings);
assertEquals("String size check", 2, strings.size());
verifyResourceExists(repo,
"string/untouched",
"string/removed_overlay");
// apply updates
RecordingLogger logger = new RecordingLogger();
// ----------------
// first set is the main one, no change here
// ----------------
// second set is the overlay one
ResourceSet overlaySet = sets.get(1);
File overlayBase = new File(root, "overlay");
File overlayValues = new File(overlayBase, "values");
// new files:
File overlayValuesNew = new File(overlayValues, "values.xml");
overlaySet.updateWith(overlayBase, overlayValuesNew, FileStatus.REMOVED, logger);
checkLogger(logger);
// validate for duplicates
resourceMerger.validateDataSets();
// check the new content.
resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
strings = items.get(ResourceType.STRING);
assertNotNull("String null check", strings);
assertEquals("String size check", 2, strings.size());
verifyResourceExists(repo,
"string/untouched",
"string/removed_overlay");
checkRemovedItems(resourceMerger);
}
public void testUpdateWithFilesVsValues() throws Exception {
File root = getIncMergeRoot("filesVsValues");
File fakeRoot = getMergedBlobFolder(root);
ResourceMerger resourceMerger = new ResourceMerger();
resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/);
checkSourceFolders(resourceMerger);
List<ResourceSet> sets = resourceMerger.getDataSets();
assertEquals(1, sets.size());
// write the content in a repo.
ResourceRepository repo = new ResourceRepository(false);
resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
// checks the initial state of the repo
Map<ResourceType, ListMultimap<String, ResourceItem>> items = repo.getItems();
ListMultimap<String, ResourceItem> layouts = items.get(ResourceType.LAYOUT);
assertNotNull("String null check", layouts);
assertEquals("String size check", 3, layouts.size());
verifyResourceExists(repo,
"layout/main",
"layout/file_replaced_by_alias",
"layout/alias_replaced_by_file");
// apply updates
RecordingLogger logger = new RecordingLogger();
// ----------------
// Load the main set
ResourceSet mainSet = sets.get(0);
File mainBase = new File(root, "main");
File mainValues = new File(mainBase, ResourceFolderType.VALUES.getName());
File mainLayout = new File(mainBase, ResourceFolderType.LAYOUT.getName());
// touched file:
File mainValuesTouched = new File(mainValues, "values.xml");
mainSet.updateWith(mainBase, mainValuesTouched, FileStatus.CHANGED, logger);
checkLogger(logger);
// new file:
File mainLayoutNew = new File(mainLayout, "alias_replaced_by_file.xml");
mainSet.updateWith(mainBase, mainLayoutNew, FileStatus.NEW, logger);
checkLogger(logger);
// removed file
File mainLayoutRemoved = new File(mainLayout, "file_replaced_by_alias.xml");
mainSet.updateWith(mainBase, mainLayoutRemoved, FileStatus.REMOVED, logger);
checkLogger(logger);
// validate for duplicates
resourceMerger.validateDataSets();
// check the new content.
resourceMerger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
layouts = items.get(ResourceType.LAYOUT);
assertNotNull("String null check", layouts);
assertEquals("String size check", 3, layouts.size());
verifyResourceExists(repo,
"layout/main",
"layout/file_replaced_by_alias",
"layout/alias_replaced_by_file");
checkRemovedItems(resourceMerger);
}
public void testUpdateFromOldFile() throws Exception {
File root = getIncMergeRoot("oldMerge");
File fakeRoot = getMergedBlobFolder(root);
ResourceMerger resourceMerger = new ResourceMerger();
assertFalse(resourceMerger.loadFromBlob(fakeRoot, false /*incrementalState*/));
}
private static void checkRemovedItems(DataMap<? extends DataItem> dataMap) {
for (DataItem item : dataMap.getDataMap().values()) {
if (item.isRemoved()) {
fail("Removed item found: " + item);
}
}
}
/**
* Creates a fake merge with given sets.
*
* the data is an array of sets.
*
* Each set is [ setName, folder1, folder2, ...]
*
*/
private static ResourceMerger createMerger(String[][] data) {
ResourceMerger merger = new ResourceMerger();
for (String[] setData : data) {
ResourceSet set = new ResourceSet(setData[0]);
merger.addDataSet(set);
for (int i = 1, n = setData.length; i < n; i++) {
set.addSource(new File(setData[i]));
}
}
return merger;
}
/**
* Returns a merger with the baseSet and baseMerge content.
*/
private static ResourceMerger getBaseResourceMerger()
throws MergingException, IOException {
File root = TestUtils.getRoot("resources", "baseMerge");
ResourceSet res = ResourceSetTest.getBaseResourceSet(false /*normalize*/);
RecordingLogger logger = new RecordingLogger();
ResourceSet overlay = new ResourceSet("overlay");
overlay.addSource(new File(root, "overlay"));
overlay.loadFromFiles(logger);
checkLogger(logger);
ResourceMerger resourceMerger = new ResourceMerger();
resourceMerger.addDataSet(res);
resourceMerger.addDataSet(overlay);
return resourceMerger;
}
/**
* Returns a merger from incMergeData initialized from the files, not from the merger
* state blog.
*/
private static ResourceMerger getIncResourceMerger(String rootName, String... sets)
throws MergingException, IOException {
File root = getIncMergeRoot(rootName);
RecordingLogger logger = new RecordingLogger();
ResourceMerger resourceMerger = new ResourceMerger();
for (String setName : sets) {
ResourceSet resourceSet = new ResourceSet(setName);
resourceSet.addSource(new File(root, setName));
resourceSet.loadFromFiles(logger);
checkLogger(logger);
resourceMerger.addDataSet(resourceSet);
}
return resourceMerger;
}
private static ResourceRepository getResourceRepository()
throws MergingException, IOException {
ResourceMerger merger = getBaseResourceMerger();
ResourceRepository repo = new ResourceRepository(false);
merger.mergeData(repo.createMergeConsumer(), true /*doCleanUp*/);
return repo;
}
private static File getIncMergeRoot(String name) throws IOException {
File root = TestUtils.getCanonicalRoot("resources", "incMergeData");
return new File(root, name);
}
private static void verifyResourceExists(ResourceRepository repository,
String... dataItemKeys) {
Map<ResourceType, ListMultimap<String, ResourceItem>> items = repository.getItems();
for (String resKey : dataItemKeys) {
String type, name, qualifier = "";
int pos = resKey.indexOf('/');
if (pos != -1) {
name = resKey.substring(pos + 1);
type = resKey.substring(0, pos);
} else {
throw new IllegalArgumentException("Invalid key " + resKey);
}
// use ? as a qualifier delimiter because of
// declare-styleable
pos = type.indexOf('?');
if (pos != -1) {
qualifier = type.substring(pos + 1);
type = type.substring(0, pos);
}
ResourceType resourceType = ResourceType.getEnum(type);
assertNotNull("Type check for " + resKey, resourceType);
Multimap<String, ResourceItem> map = items.get(resourceType);
assertNotNull("Map check for " + resKey, map);
Collection<ResourceItem> list = map.get(name);
int found = 0;
for (ResourceItem resourceItem : list) {
if (resourceItem.getName().equals(name)) {
String fileQualifier = resourceItem.getSource() != null ?
resourceItem.getSource().getQualifiers() : "";
if (qualifier.equals(fileQualifier)) {
found++;
}
}
}
assertEquals("Match for " + resKey, 1, found);
}
}
// This utility method has been pretty handy in tracking down resource repository bugs
// so keeping it for future potential use, but in unit test code so no runtime overhead
@SuppressWarnings({"deprecation", "ConstantConditions"})
public static String dumpRepository(ResourceRepository repository) {
Map<ResourceType, ListMultimap<String, ResourceItem>> mItems = repository.getMap();
Comparator<ResourceItem> comparator = new Comparator<ResourceItem>() {
@Override
public int compare(ResourceItem item1, ResourceItem item2) {
assert item1.getType() == item2.getType();
String qualifiers = item2.getSource().getQualifiers();
return item1.getSource().getQualifiers().compareTo(qualifiers);
}
};
StringBuilder sb = new StringBuilder(5000);
sb.append("Resource Map Dump For Repository ").append(repository)
.append("\n------------------------------------------------\n");
for (ResourceType type : ResourceType.values()) {
ListMultimap<String, ResourceItem> map = mItems.get(type);
if (map == null) {
continue;
}
sb.append(type.getName()).append(':').append('\n');
List<String> keys = new ArrayList<String>(map.keySet());
Collections.sort(keys);
for (String key : keys) {
List<ResourceItem> items = map.get(key);
List<ResourceItem> sorted = new ArrayList<ResourceItem>(items);
Collections.sort(sorted, comparator);
sb.append(" ").append(type.getName()).append(" ").append(key).append(": ");
boolean first = true;
for (ResourceItem item : sorted) {
if (first) {
first = false;
} else {
sb.append(", ");
}
String qualifiers = item.getSource().getQualifiers();
if (qualifiers.isEmpty()) {
qualifiers = "default";
}
sb.append(qualifiers);
}
if (!sorted.isEmpty()) {
ResourceItem item = sorted.get(0);
ResourceValue resourceValue = item.getResourceValue(repository.isFramework());
if (resourceValue == null) {
sb.append(" <no value found>");
} else {
String value = resourceValue.getValue();
if (value == null || value.isEmpty()) {
if (resourceValue instanceof StyleResourceValue) {
StyleResourceValue srv = (StyleResourceValue) resourceValue;
sb.append(" parentStyle=").append(srv.getParentStyle())
.append("\n");
for (String name : srv.getNames()) {
ItemResourceValue value1 = srv.getItem(name, false);
ItemResourceValue value2 = srv.getItem(name, true);
if (value1 != null) {
Boolean framework = false;
sb.append(" ");
sb.append(name).append(" ").append(framework).append(" ");
sb.append(" = ");
sb.append('"');
String strValue = value1.getValue();
if (strValue != null) {
sb.append(strValue.replace("\n", "\\n"));
} else {
sb.append("???");
}
sb.append('"');
}
if (value2 != null) {
Boolean framework = true;
sb.append(" ");
sb.append(name).append(" ").append(framework).append(" ");
sb.append(" = ");
sb.append('"');
String strValue = value2.getValue();
if (strValue != null) {
sb.append(strValue.replace("\n", "\\n"));
} else {
sb.append("???");
}
sb.append('"');
}
}
} else {
sb.append(" = \"\"");
}
} else {
sb.append(" = ");
sb.append('"');
sb.append(value.replace("\n", "\\n"));
sb.append('"');
}
}
}
sb.append("\n");
}
sb.append("\n\n");
}
return sb.toString();
}
}