diff --git a/apktool-cli/pom.xml b/apktool-cli/pom.xml
index c22844b4..c14f26f4 100644
--- a/apktool-cli/pom.xml
+++ b/apktool-cli/pom.xml
@@ -3,7 +3,7 @@
brut.apktool
apktool-cli
- 1.4.7-SNAPSHOT
+ 1.4.8-SNAPSHOT
jar
diff --git a/apktool-lib/pom.xml b/apktool-lib/pom.xml
index 1f0a470b..a4c75052 100644
--- a/apktool-lib/pom.xml
+++ b/apktool-lib/pom.xml
@@ -3,7 +3,7 @@
brut.apktool
apktool-lib
- 1.4.7-SNAPSHOT
+ 1.4.8-SNAPSHOT
jar
diff --git a/apktool-lib/src/main/java/brut/androlib/Androlib.java b/apktool-lib/src/main/java/brut/androlib/Androlib.java
index 077eecf6..a367c505 100644
--- a/apktool-lib/src/main/java/brut/androlib/Androlib.java
+++ b/apktool-lib/src/main/java/brut/androlib/Androlib.java
@@ -93,7 +93,7 @@ public class Androlib {
}
public void decodeManifestFull(ExtFile apkFile, File outDir,
- ResTable resTable) throws AndrolibException {
+ ResTable resTable) throws AndrolibException {
mAndRes.decodeManifest(resTable, apkFile, outDir);
}
@@ -523,4 +523,4 @@ public class Androlib {
new String[]{"AndroidManifest.xml", "res"};
private final static String[] APK_MANIFEST_FILENAMES =
new String[]{"AndroidManifest.xml"};
-}
\ No newline at end of file
+}
diff --git a/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java b/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java
index 8e0c5be1..6dee5c05 100644
--- a/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java
+++ b/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java
@@ -217,6 +217,7 @@ public class ApkDecoder {
meta.put("isFrameworkApk",
Boolean.valueOf(mAndrolib.isFrameworkApk(getResTable())));
putUsesFramework(meta);
+ putSdkInfo(meta);
}
mAndrolib.writeMetaFile(mOutDir, meta);
@@ -246,6 +247,15 @@ public class ApkDecoder {
meta.put("usesFramework", uses);
}
+ private void putSdkInfo(Map meta)
+ throws AndrolibException {
+ Map info = getResTable().getSdkInfo();
+ if (info.size() > 0) {
+ meta.put("sdkInfo", info);
+ }
+
+ }
+
private final Androlib mAndrolib;
private ExtFile mApkFile;
diff --git a/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java b/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java
index f770ddd3..199bc309 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/AndrolibResources.java
@@ -124,9 +124,8 @@ final public class AndrolibResources {
out = new FileDirectory(outDir);
LOGGER.info("Decoding AndroidManifest.xml with only framework resources...");
- fileDecoder.decode(
- inApk, "AndroidManifest.xml", out, "AndroidManifest.xml",
- "xml");
+ fileDecoder.decodeManifest(
+ inApk, "AndroidManifest.xml", out, "AndroidManifest.xml");
} catch (DirectoryException ex) {
throw new AndrolibException(ex);
@@ -148,9 +147,9 @@ final public class AndrolibResources {
out = new FileDirectory(outDir);
LOGGER.info("Decoding AndroidManifest.xml with resources...");
- fileDecoder.decode(
- inApk, "AndroidManifest.xml", out, "AndroidManifest.xml",
- "xml");
+
+ fileDecoder.decodeManifest(
+ inApk, "AndroidManifest.xml", out, "AndroidManifest.xml");
if (inApk.containsDir("res")) {
in = inApk.getDir("res");
@@ -183,6 +182,14 @@ final public class AndrolibResources {
}
}
+ public void setSdkInfo(Map map) {
+ if(map != null) {
+ mMinSdkVersion = map.get("minSdkVersion");
+ mTargetSdkVersion = map.get("targetSdkVersion");
+ mMaxSdkVersion = map.get("maxSdkVersion");
+ }
+ }
+
public void aaptPackage(File apkFile, File manifest, File resDir,
File rawDir, File assetDir, File[] include,
boolean update, boolean framework) throws AndrolibException {
@@ -193,6 +200,18 @@ final public class AndrolibResources {
if (update) {
cmd.add("-u");
}
+ if (mMinSdkVersion != null) {
+ cmd.add("--min-sdk-version");
+ cmd.add(mMinSdkVersion);
+ }
+ if (mTargetSdkVersion != null) {
+ cmd.add("--target-sdk-version");
+ cmd.add(mTargetSdkVersion);
+ }
+ if (mMaxSdkVersion != null) {
+ cmd.add("--max-sdk-version");
+ cmd.add(mMaxSdkVersion);
+ }
cmd.add("-F");
cmd.add(apkFile.getAbsolutePath());
@@ -541,4 +560,9 @@ final public class AndrolibResources {
private final static Logger LOGGER =
Logger.getLogger(AndrolibResources.class.getName());
-}
\ No newline at end of file
+
+ private String mMinSdkVersion = null;
+ private String mMaxSdkVersion = null;
+ private String mTargetSdkVersion = null;
+
+}
diff --git a/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java b/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java
index e10ae9e0..bd235509 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/data/ResConfigFlags.java
@@ -196,7 +196,9 @@ public class ResConfigFlags {
case UI_MODE_TYPE_DESK:
ret.append("-desk");
break;
-
+ case UI_MODE_TYPE_TELEVISION:
+ ret.append("-television");
+ break;
case UI_MODE_TYPE_APPLIANCE:
ret.append("-appliance");
break;
diff --git a/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java b/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java
index 1d2fecb0..58949285 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/data/ResTable.java
@@ -39,6 +39,8 @@ public class ResTable {
private String mFrameTag;
+ private Map mSdkInfo = new LinkedHashMap();
+
public ResTable() {
mAndRes = null;
}
@@ -120,4 +122,16 @@ public class ResTable {
public void setFrameTag(String tag) {
mFrameTag = tag;
}
+
+ public Map getSdkInfo() {
+ return mSdkInfo;
+ }
+
+ public void addSdkInfo(String key, String value) {
+ mSdkInfo.put(key, value);
+ }
+
+ public void clearSdkInfo() {
+ mSdkInfo.clear();
+ }
}
diff --git a/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java b/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java
index 60b4a5cd..39930619 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/data/value/ResPluralsValue.java
@@ -16,9 +16,10 @@
package brut.androlib.res.data.value;
+import brut.androlib.res.xml.ResValuesXmlSerializable;
+import brut.androlib.res.xml.ResXmlEncoders;
import brut.androlib.AndrolibException;
import brut.androlib.res.data.ResResource;
-import brut.androlib.res.xml.ResValuesXmlSerializable;
import brut.util.Duo;
import java.io.IOException;
import org.xmlpull.v1.XmlSerializer;
@@ -63,7 +64,8 @@ public class ResPluralsValue extends ResBagValue implements ResValuesXmlSerializ
}
serializer.endTag(null, "plurals");
}
-
+
+
private final ResScalarValue[] mItems;
diff --git a/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java b/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java
index 92d95344..0764d98b 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/data/value/ResScalarValue.java
@@ -18,9 +18,9 @@ package brut.androlib.res.data.value;
import brut.androlib.res.xml.ResValuesXmlSerializable;
import brut.androlib.res.xml.ResXmlEncodable;
+import brut.androlib.res.xml.ResXmlEncoders;
import brut.androlib.AndrolibException;
import brut.androlib.res.data.ResResource;
-import brut.androlib.res.xml.ResXmlEncoders;
import java.io.IOException;
import org.xmlpull.v1.XmlSerializer;
@@ -54,7 +54,7 @@ public abstract class ResScalarValue extends ResValue
}
return encodeAsResXml();
}
-
+
public String encodeAsResXmlValueExt() throws AndrolibException {
String rawValue = mRawValue;
if (rawValue != null) {
diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java
index ddb22949..661cf688 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java
@@ -139,8 +139,7 @@ public class ARSCDecoder {
checkChunkType(Header.TYPE_CONFIG);
/*typeId*/ mIn.skipInt();
int entryCount = mIn.readInt();
- /*entriesStart*/
- mIn.skipInt();
+ /*entriesStart*/ mIn.skipInt();
ResConfigFlags flags = readConfigFlags();
int[] entryOffsets = mIn.readIntArray(entryCount);
@@ -248,13 +247,13 @@ public class ARSCDecoder {
byte keyboard = mIn.readByte();
byte navigation = mIn.readByte();
byte inputFlags = mIn.readByte();
- mIn.skipBytes(1);
+ /*inputPad0*/ mIn.skipBytes(1);
short screenWidth = mIn.readShort();
short screenHeight = mIn.readShort();
short sdkVersion = mIn.readShort();
- mIn.skipBytes(2);
+ /*minorVersion, now must always be 0*/ mIn.skipBytes(2);
byte screenLayout = 0;
byte uiMode = 0;
diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java
index 11a55b97..cd7b848a 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/AXmlResourceParser.java
@@ -17,9 +17,14 @@
package brut.androlib.res.decoder;
import android.content.res.XmlResourceParser;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
+import java.io.Writer;
+
import org.xmlpull.v1.XmlPullParserException;
import android.util.TypedValue;
import brut.androlib.AndrolibException;
@@ -812,7 +817,9 @@ public class AXmlResourceParser implements XmlResourceParser {
}
int event = m_event;
- resetEventInfo();
+ if (event != START_DOCUMENT) {//keep m_lineNumber
+ resetEventInfo();
+ }
while (true) {
if (m_decreaseDepth) {
@@ -834,47 +841,49 @@ public class AXmlResourceParser implements XmlResourceParser {
chunkType = CHUNK_XML_START_TAG;
} else {
chunkType = m_reader.readInt();
- }
- if (chunkType == CHUNK_RESOURCEIDS) {
- int chunkSize = m_reader.readInt();
- if (chunkSize < 8 || (chunkSize % 4) != 0) {
- throw new IOException("Invalid resource ids size (" + chunkSize + ").");
+ if (chunkType == CHUNK_RESOURCEIDS) {
+ int chunkSize = m_reader.readInt();
+ if (chunkSize < 8 || (chunkSize % 4) != 0) {
+ throw new IOException("Invalid resource ids size (" + chunkSize + ").");
+ }
+ m_resourceIDs = m_reader.readIntArray(chunkSize / 4 - 2);
+ continue;
}
- m_resourceIDs = m_reader.readIntArray(chunkSize / 4 - 2);
- continue;
- }
- if (chunkType < CHUNK_XML_FIRST || chunkType > CHUNK_XML_LAST) {
- throw new IOException("Invalid chunk type (" + chunkType + ").");
- }
-
- // Fake START_DOCUMENT event.
- if (chunkType == CHUNK_XML_START_TAG && event == -1) {
- m_event = START_DOCUMENT;
- break;
- }
-
- // Common header.
- /*chunkSize*/ m_reader.skipInt();
- int lineNumber = m_reader.readInt();
- /*0xFFFFFFFF*/ m_reader.skipInt();
-
- if (chunkType == CHUNK_XML_START_NAMESPACE
- || chunkType == CHUNK_XML_END_NAMESPACE) {
- if (chunkType == CHUNK_XML_START_NAMESPACE) {
- int prefix = m_reader.readInt();
- int uri = m_reader.readInt();
- m_namespaces.push(prefix, uri);
- } else {
- /*prefix*/ m_reader.skipInt();
- /*uri*/ m_reader.skipInt();
- m_namespaces.pop();
+ if (chunkType < CHUNK_XML_FIRST || chunkType > CHUNK_XML_LAST) {
+ throw new IOException("Invalid chunk type (" + chunkType + ").");
}
- continue;
+
+ // Common header.
+ /*chunkSize*/ m_reader.skipInt();
+ int lineNumber = m_reader.readInt();
+ /*0xFFFFFFFF*/ m_reader.skipInt();
+
+ // Fake START_DOCUMENT event.
+ if (chunkType == CHUNK_XML_START_TAG && event == -1) {
+ m_event = START_DOCUMENT;
+ m_lineNumber = lineNumber;
+ break;
+ }
+
+ if (chunkType == CHUNK_XML_START_NAMESPACE
+ || chunkType == CHUNK_XML_END_NAMESPACE) {
+ if (chunkType == CHUNK_XML_START_NAMESPACE) {
+ int prefix = m_reader.readInt();
+ int uri = m_reader.readInt();
+ m_namespaces.push(prefix, uri);
+ } else {
+ /*prefix*/ m_reader.skipInt();
+ /*uri*/ m_reader.skipInt();
+ m_namespaces.pop();
+ }
+ continue;
+ }
+
+ m_lineNumber = lineNumber;
}
- m_lineNumber = lineNumber;
if (chunkType == CHUNK_XML_START_TAG) {
m_namespaceUri = m_reader.readInt();
@@ -893,6 +902,13 @@ public class AXmlResourceParser implements XmlResourceParser {
}
m_namespaces.increaseDepth();
m_event = START_TAG;
+ m_strings.touch(m_name, m_name);
+ for(int i = 0; i array.length)
+ max = array.length;
+ if(min < 0)
+ min = 0;
+ StringBuffer sb = new StringBuffer("[");
+ int i = min;
+ while(true) {
+ sb.append(array[i]);
+ i++;
+ if(i < max) {
+ sb.append(", ");
+ } else {
+ sb.append("]");
+ break;
+ }
+ }
+ return sb.toString();
+ }
+
+ private boolean compareAttr(int[] attr1, int[] attr2) {
+ //TODO: sort Attrs
+ /*
+ * ATTRIBUTE_IX_VALUE_TYPE == TYPE_STRING : ATTRIBUTE_IX_VALUE_STRING
+ * : ATTRIBUTE_IX_NAMESPACE_URI
+ * ATTRIBUTE_IX_NAMESPACE_URI : ATTRIBUTE_IX_NAME
+ * id
+ *
+ */
+ if(attr1[ATTRIBUTE_IX_VALUE_TYPE] == TypedValue.TYPE_STRING &&
+ attr1[ATTRIBUTE_IX_VALUE_TYPE] == attr2[ATTRIBUTE_IX_VALUE_TYPE] &&
+ //(m_strings.touch(attr1[ATTRIBUTE_IX_VALUE_STRING], m_name) ||
+ // m_strings.touch(attr2[ATTRIBUTE_IX_VALUE_STRING], m_name)) &&
+ //m_strings.touch(attr1[ATTRIBUTE_IX_VALUE_STRING], m_name) &&
+ attr1[ATTRIBUTE_IX_VALUE_STRING] != attr2[ATTRIBUTE_IX_VALUE_STRING]) {
+ return (attr1[ATTRIBUTE_IX_VALUE_STRING] < attr2[ATTRIBUTE_IX_VALUE_STRING]);
+ } else if ((attr1[ATTRIBUTE_IX_NAMESPACE_URI] == attr2[ATTRIBUTE_IX_NAMESPACE_URI]) && (attr1[ATTRIBUTE_IX_NAMESPACE_URI] != -1) &&
+ //(m_strings.touch(attr1[ATTRIBUTE_IX_NAME], m_name) ||
+ // m_strings.touch(attr2[ATTRIBUTE_IX_NAME], m_name)) &&
+ //m_strings.touch(attr1[ATTRIBUTE_IX_NAME], m_name) &&
+ (attr1[ATTRIBUTE_IX_NAME] != attr2[ATTRIBUTE_IX_NAME])) {
+ return (attr1[ATTRIBUTE_IX_NAME] < attr2[ATTRIBUTE_IX_NAME]);
+ //} else if (attr1[ATTRIBUTE_IX_NAMESPACE_URI] < attr2[ATTRIBUTE_IX_NAMESPACE_URI]) {
+ // return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void sortAttrs() {
+ int attributeCount = m_attributes.length/ATTRIBUTE_LENGHT;
+ int tmp1[][] = new int[attributeCount][];
+ int tmp2[] = null;
+ for (int i = 0; i < attributeCount;i++) {
+ tmp1[i] = new int[ATTRIBUTE_LENGHT+1];
+ for(int j = 0; j < ATTRIBUTE_LENGHT; j++) {
+ tmp1[i][j] = m_attributes[i*ATTRIBUTE_LENGHT+j];
+ }
+ tmp1[i][ATTRIBUTE_LENGHT] = i;
+ if(DBG) {
+ try {
+ if (dbgOut == null) {
+ dbgOut = new BufferedWriter(new FileWriter("C:\\res.log",false));
+ }
+ dbgOut.write("Namespace: " + getAttributeNamespace (i) +
+ ", Name: " + getAttributeName (i)+
+ ", Value: " + getAttributeValue (i) + ", Array: " +
+ formatArray(tmp1[i], 0, ATTRIBUTE_LENGHT) + "\n");
+ dbgOut.flush();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ for (int j = 1; j < attributeCount;j++) {
+ for (int i = 1; i < attributeCount;i++) {
+ if(compareAttr(tmp1[i], tmp1[i-1])) {
+ tmp2 = tmp1[i-1];
+ tmp1[i-1] = tmp1[i];
+ tmp1[i] = tmp2;
+ }
+ }
+ }
+ for (int i = 0; i < attributeCount;i++) {
+ for(int j = 0; j < ATTRIBUTE_LENGHT; j++) {
+ m_attributes[i*ATTRIBUTE_LENGHT+j] = tmp1[i][j];
+ }
+ }
+ }
+
+ private void setFirstError(AndrolibException error) {
if (mFirstError == null) {
mFirstError = error;
}
@@ -964,4 +1070,6 @@ public class AXmlResourceParser implements XmlResourceParser {
CHUNK_XML_END_TAG = 0x00100103,
CHUNK_XML_TEXT = 0x00100104,
CHUNK_XML_LAST = 0x00100104;
+ private Writer dbgOut = null;
+ private final static boolean DBG = false;
}
\ No newline at end of file
diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java
index b0c9ca48..e16f9b59 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java
@@ -116,6 +116,30 @@ public class ResFileDecoder {
}
}
+ public void decodeManifest(Directory inDir, String inFileName, Directory outDir,
+ String outFileName) throws AndrolibException {
+ InputStream in = null;
+ OutputStream out = null;
+ try {
+ in = inDir.getFileInput(inFileName);
+ out = outDir.getFileOutput(outFileName);
+ ((XmlPullStreamDecoder)mDecoders.getDecoder("xml")).decodeManifest(in, out);
+ } catch (DirectoryException ex) {
+ throw new AndrolibException(ex);
+ } finally {
+ try{
+ if (in != null) {
+ in.close();
+ }
+ if (out != null) {
+ out.close();
+ }
+ } catch (IOException ex) {
+ throw new AndrolibException(ex);
+ }
+ }
+ }
+
private final static Logger LOGGER =
Logger.getLogger(ResFileDecoder.class.getName());
-}
\ No newline at end of file
+}
diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java
index fd6c7a4c..f2e5024f 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/StringBlock.java
@@ -22,6 +22,7 @@ import brut.util.ExtDataInput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.*;
+import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -53,6 +54,10 @@ public class StringBlock {
StringBlock block = new StringBlock();
block.m_isUTF8 = (flags & UTF8_FLAG) != 0;
block.m_stringOffsets = reader.readIntArray(stringCount);
+ block.m_stringOwns = new int[stringCount];
+ for (int i=0;i= m_stringOwns.length) {
+ return false;
+ }
+ if(m_stringOwns[index] == -1) {
+ m_stringOwns[index] = own;
+ return true;
+ } else if (m_stringOwns[index] == own) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
private int[] m_stringOffsets;
private byte[] m_strings;
private int[] m_styleOffsets;
private int[] m_styles;
private boolean m_isUTF8;
+ private int[] m_stringOwns;
private static final CharsetDecoder UTF16LE_DECODER =
Charset.forName("UTF-16LE").newDecoder();
private static final CharsetDecoder UTF8_DECODER =
diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java
index b6829203..dfb6a8e0 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/XmlPullStreamDecoder.java
@@ -17,10 +17,15 @@
package brut.androlib.res.decoder;
import brut.androlib.AndrolibException;
+import brut.androlib.res.AndrolibResources;
+import brut.androlib.res.data.ResTable;
import brut.androlib.res.util.ExtXmlSerializer;
import java.io.*;
+import java.util.logging.Logger;
+
import org.xmlpull.v1.*;
import org.xmlpull.v1.wrapper.*;
+import org.xmlpull.v1.wrapper.classic.StaticXmlSerializerWrapper;
/**
* @author Ryszard Wiśniewski
@@ -37,7 +42,74 @@ public class XmlPullStreamDecoder implements ResStreamDecoder {
try {
XmlPullWrapperFactory factory = XmlPullWrapperFactory.newInstance();
XmlPullParserWrapper par = factory.newPullParserWrapper(mParser);
- XmlSerializerWrapper ser = factory.newSerializerWrapper(mSerial);
+ final ResTable resTable = ((AXmlResourceParser)mParser).getAttrDecoder().getCurrentPackage().getResTable();
+ final boolean optimizeForManifest = mOptimizeForManifest;
+ XmlSerializerWrapper ser = new StaticXmlSerializerWrapper(mSerial, factory){
+ boolean hideSdkInfo = false;
+ @Override
+ public void event(XmlPullParser pp) throws XmlPullParserException, IOException {
+ int type = pp.getEventType();
+ int newLine = pp.getLineNumber();
+ if ((!optimizeForManifest) || newLine != 0) {
+ ((ExtXmlSerializer)xs).setLineNumber(newLine, type);
+ super.event(pp);
+ } else {
+ if (type == XmlPullParser.START_TAG) {
+ if ("uses-sdk".equalsIgnoreCase(pp.getName())) {
+
+ //TODO: parse uses-sdk( and some others?)
+ /*
+ * (--version-code)
+ * (--version-name)
+ * (debuggable)
+ * (...)
+ * --min-sdk-version
+ * --target-sdk-version
+ * --max-sdk-version
+ */
+ try {
+ hideSdkInfo = parseAttr(pp);
+ if(hideSdkInfo) {
+ return;
+ }
+ } catch (AndrolibException e) {}
+ }
+ LOGGER.warning("Found generated line but parse failed, output it in xml.");
+ } else if (hideSdkInfo && type == XmlPullParser.END_TAG &&
+ "uses-sdk".equalsIgnoreCase(pp.getName())) {
+ return;
+ }
+ //((ExtXmlSerializer)xs).setLineNumber(newLine, type);
+ super.event(pp);
+ }
+ }
+
+ private boolean parseAttr(XmlPullParser pp) throws AndrolibException {
+ ResTable restable = resTable;
+ for (int i = 0; i < pp.getAttributeCount(); i++) {
+ final String a_ns = "http://schemas.android.com/apk/res/android";
+ String ns = pp.getAttributeNamespace (i);
+ if (a_ns.equalsIgnoreCase(ns)) {
+ String name = pp.getAttributeName (i);
+ String value = pp.getAttributeValue (i);
+ if (name != null && value != null) {
+ if (name.equalsIgnoreCase("minSdkVersion") ||
+ name.equalsIgnoreCase("targetSdkVersion") ||
+ name.equalsIgnoreCase("maxSdkVersion")) {
+ restable.addSdkInfo(name, value);
+ } else {
+ restable.clearSdkInfo();
+ return false;//Found unknown flags
+ }
+ }
+ } else {
+ resTable.clearSdkInfo();
+ return false;//Found unknown flags
+ }
+ }
+ return true;
+ }
+ };//factory.newSerializerWrapper(mSerial);
par.setInput(in, null);
ser.setOutput(out, null);
@@ -54,6 +126,21 @@ public class XmlPullStreamDecoder implements ResStreamDecoder {
}
}
+ public void decodeManifest(InputStream in, OutputStream out)
+ throws AndrolibException {
+ mOptimizeForManifest = true;
+ try {
+ decode(in, out);
+ } finally {
+ mOptimizeForManifest = false;
+ }
+ }
+
private final XmlPullParser mParser;
private final ExtXmlSerializer mSerial;
+
+ private boolean mOptimizeForManifest = false;
+
+ private final static Logger LOGGER =
+ Logger.getLogger(XmlPullStreamDecoder.class.getName());
}
diff --git a/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java b/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java
index 3beed1f6..cd9a83a0 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/util/ExtMXSerializer.java
@@ -17,7 +17,10 @@
package brut.androlib.res.util;
import java.io.*;
+
import org.xmlpull.mxp1_serializer.MXSerializer;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
/**
* @author Ryszard Wiśniewski
@@ -26,9 +29,11 @@ public class ExtMXSerializer extends MXSerializer implements ExtXmlSerializer {
@Override
public void startDocument(String encoding, Boolean standalone) throws
IOException, IllegalArgumentException, IllegalStateException {
- super.startDocument(encoding != null ? encoding : mDefaultEncoding,
- standalone);
- this.newLine();
+ if (!enableLineOpt || mNewLine >= 1) {
+ super.startDocument(encoding != null ? encoding : mDefaultEncoding,
+ standalone);
+ this.newLine();
+ }
}
@Override
@@ -66,13 +71,129 @@ public class ExtMXSerializer extends MXSerializer implements ExtXmlSerializer {
public ExtXmlSerializer newLine() throws IOException {
super.out.write(lineSeparator);
+ mCurLine ++;
+ dbg("Nline: " + mCurLine);
return this;
}
+ //XmlPullParser.START_TAG("")
+ // if(doIndent) writeIndent();
+ @Override
+ protected void writeIndent() throws IOException {
+ if(!enableLineOpt || mCurLine < mNewLine) {
+ super.writeIndent();
+ mCurLine ++;
+ dbg("Iline: "+mCurLine);
+ }
+ }
+
+ @Override
+ public ExtXmlSerializer setLineNumber(int newLine, int event) throws IOException {
+ dbg(/*"curline: " + mCurLine + */", event: " + XmlPullParser.TYPES[event] +
+ ", newline: " + newLine);
+ if (newLine == -1) {//Can`t fount line number info
+ enableLineOpt = false;
+ } else {
+ enableLineOpt = true;
+ if (event == XmlPullParser.START_DOCUMENT) {
+ if (newLine > 1) {
+ mNewLine = 1;
+ } else {
+ mNewLine = 0;
+ }
+ } else {
+ mNewLine = newLine;
+ if (!startTagIncomplete) {//XmlPullParser.START_TAG("<")
+ dbg(", old event:"+XmlPullParser.TYPES[mLastEvent]);
+ if(mLastEvent != XmlPullParser.END_TAG) {
+ moveToLine (newLine);
+ } else {
+ moveToLine (newLine - 1);
+ }
+ }
+ }
+ mLastEvent = event;
+ }
+ return this;
+ }
+
+ //XmlPullParser.END_TAG(" />") and XmlPullParser.START_TAG("><")
+ @Override
+ protected void writeNamespaceDeclarations() throws IOException {
+ super.writeNamespaceDeclarations();
+ if (enableLineOpt) {
+ if (mLastEvent == XmlPullParser.END_TAG) {
+ moveToLine (mNewLine);
+ } else {
+ moveToLine (mNewLine - 1);
+ }
+ }
+ }
+
+ private ExtXmlSerializer moveToLine(int newLine) throws IOException {
+ int addLines = newLine - mCurLine;
+ dbg(", addLines: " + addLines);
+ for (; addLines > 0; addLines --) {
+ newLine();
+ }
+ return this;
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ mCurLine = 1;
+ mLastEvent = XmlPullParser.START_DOCUMENT;
+ enableLineOpt = false;
+ }
+
public void setDisabledAttrEscape(boolean disabled) {
mIsDisabledAttrEscape = disabled;
}
+ @Override
+ public XmlSerializer text(String text) throws IOException {
+ if (enableLineOpt) {
+ mCurLine += (getTextLineNum(text) - 1);
+ }
+ return super.text(text);
+ }
+
+ private int getTextLineNum(String text) {
+ String str = "." + text + ".";
+ int linenum = str.split("\\n").length + str.split("\\r").length
+ - str.split("\\n\\r").length;//(Unix(LF)-1) + (Mac(CR)-1) - (Win(CRLF)-1) + 1
+ return linenum;
+ }
+
+ @Override
+ public XmlSerializer text(char[] buf, int start, int len)
+ throws IOException {
+ if (enableLineOpt) {
+ mCurLine += (getTextLineNum(new String(buf, start, len)) - 1);
+ }
+ return super.text(buf, start, len);
+ }
+
+ @Override
+ public void ignorableWhitespace(String text) throws IOException {
+ if (enableLineOpt) {
+ mCurLine += (getTextLineNum(text) - 1);
+ }
+ super.ignorableWhitespace(text);
+ }
+
private String mDefaultEncoding;
private boolean mIsDisabledAttrEscape = false;
+ private int mCurLine;
+ private int mNewLine;
+ private int mLastEvent;
+ private boolean enableLineOpt = false;
+ private final static boolean DBG = false;
+
+ public ExtXmlSerializer dbg(String str) throws IOException {
+ if(DBG)
+ super.out.write("");
+ return this;
+ }
}
diff --git a/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java b/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java
index 3c8ffed6..8fc75f8b 100644
--- a/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java
+++ b/apktool-lib/src/main/java/brut/androlib/res/util/ExtXmlSerializer.java
@@ -26,6 +26,8 @@ public interface ExtXmlSerializer extends XmlSerializer {
public ExtXmlSerializer newLine() throws IOException;
public void setDisabledAttrEscape(boolean disabled);
+ public ExtXmlSerializer setLineNumber(int newLine, int event) throws IOException;
+ public ExtXmlSerializer dbg(String str) throws IOException;
public static final String PROPERTY_SERIALIZER_INDENTATION =
"http://xmlpull.org/v1/doc/properties.html#serializer-indentation";
diff --git a/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java b/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java
index c243883f..92eb3f50 100644
--- a/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java
+++ b/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java
@@ -44,7 +44,7 @@ public class SmaliDecoder {
baksmali.disassembleDexFile(mApkFile.getAbsolutePath(),
new DexFile(mApkFile), false, mOutDir.getAbsolutePath(), null,
null, null, false, true, true, true, false, false,
- mDebug ? main.FULLMERGE : 0, false, false, null);
+ mDebug ? main.ALLPRE : 0, false, false, null);
} catch (IOException ex) {
throw new AndrolibException(ex);
}
diff --git a/apktool-lib/src/main/java/org/xmlpull/mxp1_serializer/MXSerializer.java b/apktool-lib/src/main/java/org/xmlpull/mxp1_serializer/MXSerializer.java
new file mode 100644
index 00000000..47af7c2b
--- /dev/null
+++ b/apktool-lib/src/main/java/org/xmlpull/mxp1_serializer/MXSerializer.java
@@ -0,0 +1,1160 @@
+/**
+ * Copyright 2011 Ryszard Wiśniewski
+ *
+ * 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 org.xmlpull.mxp1_serializer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+import org.xmlpull.v1.XmlSerializer;
+
+/**
+ * Implementation of XmlSerializer interface from XmlPull V1 API.
+ * This implementation is optimzied for performance and low memory footprint.
+ *
+ * Implemented features:
+ * - FEATURE_NAMES_INTERNED - when enabled all returned names
+ * (namespaces, prefixes) will be interned and it is required that
+ * all names passed as arguments MUST be interned
+ *
- FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE
+ *
+ * Implemented properties:
+ * - PROPERTY_SERIALIZER_INDENTATION
+ *
- PROPERTY_SERIALIZER_LINE_SEPARATOR
+ *
+ *
+ */
+public class MXSerializer implements XmlSerializer {
+ protected final static String XML_URI = "http://www.w3.org/XML/1998/namespace";
+ protected final static String XMLNS_URI = "http://www.w3.org/2000/xmlns/";
+ private static final boolean TRACE_SIZING = false;
+ private static final boolean TRACE_ESCAPING = false;
+
+ protected final String FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE =
+ "http://xmlpull.org/v1/doc/features.html#serializer-attvalue-use-apostrophe";
+ protected final String FEATURE_NAMES_INTERNED =
+ "http://xmlpull.org/v1/doc/features.html#names-interned";
+ protected final String PROPERTY_SERIALIZER_INDENTATION =
+ "http://xmlpull.org/v1/doc/properties.html#serializer-indentation";
+ protected final String PROPERTY_SERIALIZER_LINE_SEPARATOR =
+ "http://xmlpull.org/v1/doc/properties.html#serializer-line-separator";
+ protected final static String PROPERTY_LOCATION =
+ "http://xmlpull.org/v1/doc/properties.html#location";
+
+ // properties/features
+ protected boolean namesInterned;
+ protected boolean attributeUseApostrophe;
+ protected String indentationString = null; //" ";
+ protected String lineSeparator = "\n";
+
+ protected String location;
+ protected Writer out;
+
+ protected int autoDeclaredPrefixes;
+
+ protected int depth = 0;
+
+ // element stack
+ protected String elNamespace[] = new String[ 2 ];
+ protected String elName[] = new String[ elNamespace.length ];
+ protected String elPrefix[] = new String[ elNamespace.length ];
+ protected int elNamespaceCount[] = new int[ elNamespace.length ];
+
+ //namespace stack
+ protected int namespaceEnd = 0;
+ protected String namespacePrefix[] = new String[ 8 ];
+ protected String namespaceUri[] = new String[ namespacePrefix.length ];
+
+ protected boolean finished;
+ protected boolean pastRoot;
+ protected boolean setPrefixCalled;
+ protected boolean startTagIncomplete;
+
+ protected boolean doIndent;
+ protected boolean seenTag;
+
+ protected boolean seenBracket;
+ protected boolean seenBracketBracket;
+
+ // buffer output if neede to write escaped String see text(String)
+ private static final int BUF_LEN = Runtime.getRuntime().freeMemory() > 1000000L ? 8*1024 : 256;
+ protected char buf[] = new char[ BUF_LEN ];
+
+
+ protected static final String precomputedPrefixes[];
+
+ static {
+ precomputedPrefixes = new String[32]; //arbitrary number ...
+ for (int i = 0; i < precomputedPrefixes.length; i++)
+ {
+ precomputedPrefixes[i] = ("n"+i).intern();
+ }
+ }
+
+ private boolean checkNamesInterned = false;
+
+ private void checkInterning(String name) {
+ if(namesInterned && name != name.intern()) {
+ throw new IllegalArgumentException(
+ "all names passed as arguments must be interned"
+ +"when NAMES INTERNED feature is enabled");
+ }
+ }
+
+ protected void reset() {
+ location = null;
+ out = null;
+ autoDeclaredPrefixes = 0;
+ depth = 0;
+
+ // nullify references on all levels to allow it to be GCed
+ for (int i = 0; i < elNamespaceCount.length; i++)
+ {
+ elName[ i ] = null;
+ elPrefix[ i ] = null;
+ elNamespace[ i ] = null;
+ elNamespaceCount[ i ] = 2;
+ }
+
+
+ namespaceEnd = 0;
+
+
+ //NOTE: no need to intern() as all literal strings and string-valued constant expressions
+ //are interned. String literals are defined in 3.10.5 of the Java Language Specification
+ // just checking ...
+ //assert "xmlns" == "xmlns".intern();
+ //assert XMLNS_URI == XMLNS_URI.intern();
+
+ //TODO: how to prevent from reporting this namespace?
+ // this is special namespace declared for consistensy with XML infoset
+ namespacePrefix[ namespaceEnd ] = "xmlns";
+ namespaceUri[ namespaceEnd ] = XMLNS_URI;
+ ++namespaceEnd;
+
+ namespacePrefix[ namespaceEnd ] = "xml";
+ namespaceUri[ namespaceEnd ] = XML_URI;
+ ++namespaceEnd;
+
+ finished = false;
+ pastRoot = false;
+ setPrefixCalled = false;
+ startTagIncomplete = false;
+ //doIntent is not changed
+ seenTag = false;
+
+ seenBracket = false;
+ seenBracketBracket = false;
+ }
+
+
+ protected void ensureElementsCapacity() {
+ final int elStackSize = elName.length;
+ //assert (depth + 1) >= elName.length;
+ // we add at least one extra slot ...
+ final int newSize = (depth >= 7 ? 2 * depth : 8) + 2; // = lucky 7 + 1 //25
+ if(TRACE_SIZING) {
+ System.err.println(
+ getClass().getName()+" elStackSize "+elStackSize+" ==> "+newSize);
+ }
+ final boolean needsCopying = elStackSize > 0;
+ String[] arr = null;
+ // reuse arr local variable slot
+ arr = new String[newSize];
+ if(needsCopying) System.arraycopy(elName, 0, arr, 0, elStackSize);
+ elName = arr;
+
+ arr = new String[newSize];
+ if(needsCopying) System.arraycopy(elPrefix, 0, arr, 0, elStackSize);
+ elPrefix = arr;
+
+ arr = new String[newSize];
+ if(needsCopying) System.arraycopy(elNamespace, 0, arr, 0, elStackSize);
+ elNamespace = arr;
+
+ final int[] iarr = new int[newSize];
+ if(needsCopying) {
+ System.arraycopy(elNamespaceCount, 0, iarr, 0, elStackSize);
+ } else {
+ // special initialization
+ iarr[0] = 0;
+ }
+ elNamespaceCount = iarr;
+ }
+
+ protected void ensureNamespacesCapacity() { //int size) {
+ //int namespaceSize = namespacePrefix != null ? namespacePrefix.length : 0;
+ //assert (namespaceEnd >= namespacePrefix.length);
+
+ //if(size >= namespaceSize) {
+ //int newSize = size > 7 ? 2 * size : 8; // = lucky 7 + 1 //25
+ final int newSize = namespaceEnd > 7 ? 2 * namespaceEnd : 8;
+ if(TRACE_SIZING) {
+ System.err.println(
+ getClass().getName()+" namespaceSize "+namespacePrefix.length+" ==> "+newSize);
+ }
+ final String[] newNamespacePrefix = new String[newSize];
+ final String[] newNamespaceUri = new String[newSize];
+ if(namespacePrefix != null) {
+ System.arraycopy(
+ namespacePrefix, 0, newNamespacePrefix, 0, namespaceEnd);
+ System.arraycopy(
+ namespaceUri, 0, newNamespaceUri, 0, namespaceEnd);
+ }
+ namespacePrefix = newNamespacePrefix;
+ namespaceUri = newNamespaceUri;
+
+ // TODO use hashes for quick namespace->prefix lookups
+ // if( ! allStringsInterned ) {
+ // int[] newNamespacePrefixHash = new int[newSize];
+ // if(namespacePrefixHash != null) {
+ // System.arraycopy(
+ // namespacePrefixHash, 0, newNamespacePrefixHash, 0, namespaceEnd);
+ // }
+ // namespacePrefixHash = newNamespacePrefixHash;
+ // }
+ //prefixesSize = newSize;
+ // ////assert nsPrefixes.length > size && nsPrefixes.length == newSize
+ //}
+ }
+
+
+ public void setFeature(String name,
+ boolean state) throws IllegalArgumentException, IllegalStateException
+ {
+ if(name == null) {
+ throw new IllegalArgumentException("feature name can not be null");
+ }
+ if(FEATURE_NAMES_INTERNED.equals(name)) {
+ namesInterned = state;
+ } else if(FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals(name)) {
+ attributeUseApostrophe = state;
+ } else {
+ throw new IllegalStateException("unsupported feature "+name);
+ }
+ }
+
+ public boolean getFeature(String name) throws IllegalArgumentException
+ {
+ if(name == null) {
+ throw new IllegalArgumentException("feature name can not be null");
+ }
+ if(FEATURE_NAMES_INTERNED.equals(name)) {
+ return namesInterned;
+ } else if(FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals(name)) {
+ return attributeUseApostrophe;
+ } else {
+ return false;
+ }
+ }
+
+ // precomputed variables to simplify writing indentation
+ protected int offsetNewLine;
+ protected int indentationJump;
+ protected char[] indentationBuf;
+ protected int maxIndentLevel;
+ protected boolean writeLineSepartor; //should end-of-line be written
+ protected boolean writeIndentation; // is indentation used?
+
+ /**
+ * For maximum efficiency when writing indents the required output is pre-computed
+ * This is internal function that recomputes buffer after user requested chnages.
+ */
+ protected void rebuildIndentationBuf() {
+ if(doIndent == false) return;
+ final int maxIndent = 65; //hardcoded maximum indentation size in characters
+ int bufSize = 0;
+ offsetNewLine = 0;
+ if(writeLineSepartor) {
+ offsetNewLine = lineSeparator.length();
+ bufSize += offsetNewLine;
+ }
+ maxIndentLevel = 0;
+ if(writeIndentation) {
+ indentationJump = indentationString.length();
+ maxIndentLevel = maxIndent / indentationJump;
+ bufSize += maxIndentLevel * indentationJump;
+ }
+ if(indentationBuf == null || indentationBuf.length < bufSize) {
+ indentationBuf = new char[bufSize + 8];
+ }
+ int bufPos = 0;
+ if(writeLineSepartor) {
+ for (int i = 0; i < lineSeparator.length(); i++)
+ {
+ indentationBuf[ bufPos++ ] = lineSeparator.charAt(i);
+ }
+ }
+ if(writeIndentation) {
+ for (int i = 0; i < maxIndentLevel; i++)
+ {
+ for (int j = 0; j < indentationString.length(); j++)
+ {
+ indentationBuf[ bufPos++ ] = indentationString.charAt(j);
+ }
+ }
+ }
+ }
+
+ // if(doIndent) writeIndent();
+ protected void writeIndent() throws IOException {
+ final int start = writeLineSepartor ? 0 : offsetNewLine;
+ final int level = (depth > maxIndentLevel) ? maxIndentLevel : depth;
+ out.write( indentationBuf, start, ( (level - 1) * indentationJump) + offsetNewLine);
+ }
+
+ public void setProperty(String name,
+ Object value) throws IllegalArgumentException, IllegalStateException
+ {
+ if(name == null) {
+ throw new IllegalArgumentException("property name can not be null");
+ }
+ if(PROPERTY_SERIALIZER_INDENTATION.equals(name)) {
+ indentationString = (String)value;
+ } else if(PROPERTY_SERIALIZER_LINE_SEPARATOR.equals(name)) {
+ lineSeparator = (String)value;
+ } else if(PROPERTY_LOCATION.equals(name)) {
+ location = (String) value;
+ } else {
+ throw new IllegalStateException("unsupported property "+name);
+ }
+ writeLineSepartor = lineSeparator != null && lineSeparator.length() > 0;
+ writeIndentation = indentationString != null && indentationString.length() > 0;
+ // optimize - do not write when nothing to write ...
+ doIndent = indentationString != null && (writeLineSepartor || writeIndentation);
+ //NOTE: when indentationString == null there is no indentation
+ // (even though writeLineSeparator may be true ...)
+ rebuildIndentationBuf();
+ seenTag = false; // for consistency
+ }
+
+ public Object getProperty(String name) throws IllegalArgumentException
+ {
+ if(name == null) {
+ throw new IllegalArgumentException("property name can not be null");
+ }
+ if(PROPERTY_SERIALIZER_INDENTATION.equals(name)) {
+ return indentationString;
+ } else if(PROPERTY_SERIALIZER_LINE_SEPARATOR.equals(name)) {
+ return lineSeparator;
+ } else if(PROPERTY_LOCATION.equals(name)) {
+ return location;
+ } else {
+ return null;
+ }
+ }
+
+ private String getLocation() {
+ return location != null ? " @"+location : "";
+ }
+
+ // this is special method that can be accessed directly to retrieve Writer serializer is using
+ public Writer getWriter()
+ {
+ return out;
+ }
+
+ public void setOutput(Writer writer)
+ {
+ reset();
+ out = writer;
+ }
+
+ public void setOutput(OutputStream os, String encoding) throws IOException
+ {
+ if(os == null) throw new IllegalArgumentException("output stream can not be null");
+ reset();
+ if(encoding != null) {
+ out = new OutputStreamWriter(os, encoding);
+ } else {
+ out = new OutputStreamWriter(os);
+ }
+ }
+
+ public void startDocument (String encoding, Boolean standalone) throws IOException
+ {
+ char apos = attributeUseApostrophe ? '\'' : '"';
+ if(attributeUseApostrophe) {
+ out.write("");
+ }
+
+ public void endDocument() throws IOException
+ {
+ // close all unclosed tag;
+ while(depth > 0) {
+ endTag(elNamespace[ depth ], elName[ depth ]);
+ }
+ //assert depth == 0;
+ //assert startTagIncomplete == false;
+ finished = pastRoot = startTagIncomplete = true;
+ out.flush();
+ }
+
+ public void setPrefix(String prefix, String namespace) throws IOException
+ {
+ if(startTagIncomplete) closeStartTag();
+ //assert prefix != null;
+ //assert namespace != null;
+ if (prefix == null) {
+ prefix = "";
+ }
+ if(!namesInterned) {
+ prefix = prefix.intern(); //will throw NPE if prefix==null
+ } else if(checkNamesInterned) {
+ checkInterning(prefix);
+ } else if(prefix == null) {
+ throw new IllegalArgumentException("prefix must be not null"+getLocation());
+ }
+
+ //check that prefix is not duplicated ...
+ for (int i = elNamespaceCount[ depth ]; i < namespaceEnd; i++)
+ {
+ if(prefix == namespacePrefix[ i ]) {
+ throw new IllegalStateException("duplicated prefix "+printable(prefix)+getLocation());
+ }
+ }
+
+ if(!namesInterned) {
+ namespace = namespace.intern();
+ } else if(checkNamesInterned) {
+ checkInterning(namespace);
+ } else if(namespace == null) {
+ throw new IllegalArgumentException("namespace must be not null"+getLocation());
+ }
+
+ if(namespaceEnd >= namespacePrefix.length) {
+ ensureNamespacesCapacity();
+ }
+ namespacePrefix[ namespaceEnd ] = prefix;
+ namespaceUri[ namespaceEnd ] = namespace;
+ ++namespaceEnd;
+ setPrefixCalled = true;
+ }
+
+ protected String lookupOrDeclarePrefix( String namespace ) {
+ return getPrefix(namespace, true);
+ }
+
+ public String getPrefix(String namespace, boolean generatePrefix)
+ {
+ return getPrefix(namespace, generatePrefix, false);
+ }
+
+ protected String getPrefix(String namespace, boolean generatePrefix, boolean nonEmpty)
+ {
+ //assert namespace != null;
+ if(!namesInterned) {
+ // when String is interned we can do much faster namespace stack lookups ...
+ namespace = namespace.intern();
+ } else if(checkNamesInterned) {
+ checkInterning(namespace);
+ //assert namespace != namespace.intern();
+ }
+ if(namespace == null) {
+ throw new IllegalArgumentException("namespace must be not null"+getLocation());
+ } else if(namespace.length() == 0) {
+ throw new IllegalArgumentException("default namespace cannot have prefix"+getLocation());
+ }
+
+ // first check if namespace is already in scope
+ for (int i = namespaceEnd - 1; i >= 0 ; --i)
+ {
+ if(namespace == namespaceUri[ i ]) {
+ final String prefix = namespacePrefix[ i ];
+ if(nonEmpty && prefix.length() == 0) continue;
+ // now check that prefix is still in scope
+ for (int p = namespaceEnd - 1; p > i ; --p)
+ {
+ if(prefix == namespacePrefix[ p ])
+ continue; // too bad - prefix is redeclared with different namespace
+ }
+ return prefix;
+ }
+ }
+
+ // so not found it ...
+ if(!generatePrefix) {
+ return null;
+ }
+ return generatePrefix(namespace);
+ }
+
+ private String generatePrefix(String namespace) {
+ //assert namespace == namespace.intern();
+ while(true) {
+ ++autoDeclaredPrefixes;
+ //fast lookup uses table that was pre-initialized in static{} ....
+ final String prefix = autoDeclaredPrefixes < precomputedPrefixes.length
+ ? precomputedPrefixes[autoDeclaredPrefixes] : ("n"+autoDeclaredPrefixes).intern();
+ // make sure this prefix is not declared in any scope (avoid hiding in-scope prefixes)!
+ for (int i = namespaceEnd - 1; i >= 0 ; --i)
+ {
+ if(prefix == namespacePrefix[ i ]) {
+ continue; // prefix is already declared - generate new and try again
+ }
+ }
+ // declare prefix
+
+ if(namespaceEnd >= namespacePrefix.length) {
+ ensureNamespacesCapacity();
+ }
+ namespacePrefix[ namespaceEnd ] = prefix;
+ namespaceUri[ namespaceEnd ] = namespace;
+ ++namespaceEnd;
+
+ return prefix;
+ }
+ }
+
+ public int getDepth()
+ {
+ return depth;
+ }
+
+ public String getNamespace ()
+ {
+ return elNamespace[depth];
+ }
+
+ public String getName()
+ {
+ return elName[depth];
+ }
+
+ public XmlSerializer startTag (String namespace, String name) throws IOException
+ {
+ if(startTagIncomplete) {
+ closeStartTag();
+ }
+ seenBracket = seenBracketBracket = false;
+ ++depth;
+ if(doIndent && depth > 0 && seenTag) {
+ writeIndent();
+ }
+ seenTag = true;
+ setPrefixCalled = false;
+ startTagIncomplete = true;
+ if( (depth + 1) >= elName.length) {
+ ensureElementsCapacity();
+ }
+ ////assert namespace != null;
+
+ if(checkNamesInterned && namesInterned) checkInterning(namespace);
+ elNamespace[ depth ] = (namesInterned || namespace == null) ? namespace : namespace.intern();
+ //assert name != null;
+ //elName[ depth ] = name;
+ if(checkNamesInterned && namesInterned) checkInterning(name);
+ elName[ depth ] = (namesInterned || name == null) ? name : name.intern();
+ if(out == null) {
+ throw new IllegalStateException("setOutput() must called set before serialization can start");
+ }
+ out.write('<');
+ if(namespace != null) {
+ if(namespace.length() > 0) {
+ //ALEK: in future make this algo a feature on serializer
+ String prefix = null;
+ if(depth > 0 && (namespaceEnd - elNamespaceCount[depth-1]) == 1) {
+ // if only one prefix was declared un-declare it if the prefix is already declared on parent el with the same URI
+ String uri = namespaceUri[namespaceEnd-1];
+ if(uri == namespace || uri.equals(namespace)) {
+ String elPfx = namespacePrefix[namespaceEnd-1];
+ // 2 == to skip predefined namesapces (xml and xmlns ...)
+ for(int pos = elNamespaceCount[depth-1] - 1; pos >= 2; --pos ) {
+ String pf = namespacePrefix[pos];
+ if(pf == elPfx || pf.equals(elPfx)) {
+ String n = namespaceUri[pos];
+ if(n == uri || n.equals(uri)) {
+ --namespaceEnd; //un-declare namespace: this is kludge!
+ prefix = elPfx;
+ }
+ break;
+ }
+ }
+ }
+ }
+ if(prefix == null) {
+ prefix = lookupOrDeclarePrefix( namespace );
+ }
+ //assert prefix != null;
+ // make sure that default ("") namespace to not print ":"
+ if(prefix.length() > 0) {
+ elPrefix[ depth ] = prefix;
+ out.write(prefix);
+ out.write(':');
+ } else {
+ elPrefix[ depth ] = "";
+ }
+ } else {
+ // make sure that default namespace can be declared
+ for (int i = namespaceEnd - 1; i >= 0 ; --i) {
+ if(namespacePrefix[ i ] == "") {
+ final String uri = namespaceUri[ i ];
+ if(uri == null) {
+ // declare default namespace
+ setPrefix("", "");
+ } else if(uri.length() > 0) {
+ throw new IllegalStateException(
+ "start tag can not be written in empty default namespace "+
+ "as default namespace is currently bound to '"+uri+"'"+getLocation());
+ }
+ break;
+ }
+ }
+ elPrefix[ depth ] = "";
+ }
+ } else {
+ elPrefix[ depth ] = "";
+ }
+ out.write(name);
+ return this;
+ }
+
+ public XmlSerializer attribute (String namespace, String name,
+ String value) throws IOException
+ {
+ if(!startTagIncomplete) {
+ throw new IllegalArgumentException("startTag() must be called before attribute()"+getLocation());
+ }
+ //assert setPrefixCalled == false;
+ out.write(' ');
+ ////assert namespace != null;
+ if(namespace != null && namespace.length() > 0) {
+ //namespace = namespace.intern();
+ if(!namesInterned) {
+ namespace = namespace.intern();
+ } else if(checkNamesInterned) {
+ checkInterning(namespace);
+ }
+ String prefix = getPrefix( namespace, false, true );
+ //assert( prefix != null);
+ //if(prefix.length() == 0) {
+ if(prefix == null) {
+ // needs to declare prefix to hold default namespace
+ //NOTE: attributes such as a='b' are in NO namespace
+ prefix = generatePrefix(namespace);
+ }
+ out.write(prefix);
+ out.write(':');
+ // if(prefix.length() > 0) {
+ // out.write(prefix);
+ // out.write(':');
+ // }
+ }
+ //assert name != null;
+ out.write(name);
+ out.write('=');
+ //assert value != null;
+ out.write( attributeUseApostrophe ? '\'' : '"');
+ writeAttributeValue(value, out);
+ out.write( attributeUseApostrophe ? '\'' : '"');
+ return this;
+ }
+
+ protected void closeStartTag() throws IOException {
+ if(finished) {
+ throw new IllegalArgumentException("trying to write past already finished output"+getLocation());
+ }
+ if(seenBracket) {
+ seenBracket = seenBracketBracket = false;
+ }
+ if( startTagIncomplete || setPrefixCalled ) {
+ if(setPrefixCalled) {
+ throw new IllegalArgumentException(
+ "startTag() must be called immediately after setPrefix()"+getLocation());
+ }
+ if(!startTagIncomplete) {
+ throw new IllegalArgumentException("trying to close start tag that is not opened"+getLocation());
+ }
+
+ // write all namespace delcarations!
+ writeNamespaceDeclarations();
+ out.write('>');
+ elNamespaceCount[ depth ] = namespaceEnd;
+ startTagIncomplete = false;
+ }
+ }
+
+ protected void writeNamespaceDeclarations() throws IOException
+ {
+ //int start = elNamespaceCount[ depth - 1 ];
+ for (int i = elNamespaceCount[ depth - 1 ]; i < namespaceEnd; i++)
+ {
+ if(doIndent && namespaceUri[ i ].length() > 40) {
+ writeIndent();
+ out.write(" ");
+ }
+ if(namespacePrefix[ i ] != "") {
+ out.write(" xmlns:");
+ out.write(namespacePrefix[ i ]);
+ out.write('=');
+ } else {
+ out.write(" xmlns=");
+ }
+ out.write( attributeUseApostrophe ? '\'' : '"');
+
+ //NOTE: escaping of namespace value the same way as attributes!!!!
+ writeAttributeValue(namespaceUri[ i ], out);
+
+ out.write( attributeUseApostrophe ? '\'' : '"');
+ }
+ }
+
+ public XmlSerializer endTag(String namespace, String name) throws IOException
+ {
+ // check that level is valid
+ ////assert namespace != null;
+ //if(namespace != null) {
+ // namespace = namespace.intern();
+ //}
+ seenBracket = seenBracketBracket = false;
+ if(namespace != null) {
+ if(!namesInterned) {
+ namespace = namespace.intern();
+ } else if(checkNamesInterned) {
+ checkInterning(namespace);
+ }
+ }
+
+ if(namespace != elNamespace[ depth ])
+ {
+ throw new IllegalArgumentException(
+ "expected namespace "+printable(elNamespace[ depth ])
+ +" and not "+printable(namespace)+getLocation());
+ }
+ if(name == null) {
+ throw new IllegalArgumentException("end tag name can not be null"+getLocation());
+ }
+ if(checkNamesInterned && namesInterned) {
+ checkInterning(name);
+ }
+ String startTagName = elName[ depth ];
+ if((!namesInterned && !name.equals(startTagName))
+ || (namesInterned && name != startTagName ))
+ {
+ throw new IllegalArgumentException(
+ "expected element name "+printable(elName[ depth ])+" and not "+printable(name)+getLocation());
+ }
+ if(startTagIncomplete) {
+ writeNamespaceDeclarations();
+ out.write(" />"); //space is added to make it easier to work in XHTML!!!
+ --depth;
+ } else {
+ //assert startTagIncomplete == false;
+ if(doIndent && seenTag) { writeIndent(); }
+ out.write("");
+ String startTagPrefix = elPrefix[ depth ];
+ if(startTagPrefix.length() > 0) {
+ out.write(startTagPrefix);
+ out.write(':');
+ }
+
+ // if(namespace != null && namespace.length() > 0) {
+ // //TODO prefix should be alredy known from matching start tag ...
+ // final String prefix = lookupOrDeclarePrefix( namespace );
+ // //assert( prefix != null);
+ // if(prefix.length() > 0) {
+ // out.write(prefix);
+ // out.write(':');
+ // }
+ // }
+ out.write(name);
+ out.write('>');
+ --depth;
+ }
+ namespaceEnd = elNamespaceCount[ depth ];
+ startTagIncomplete = false;
+ seenTag = true;
+ return this;
+ }
+
+ public XmlSerializer text (String text) throws IOException
+ {
+ //assert text != null;
+ if(startTagIncomplete || setPrefixCalled) closeStartTag();
+ if(doIndent && seenTag) seenTag = false;
+ writeElementContent(text, out);
+ return this;
+ }
+
+ public XmlSerializer text (char [] buf, int start, int len) throws IOException
+ {
+ if(startTagIncomplete || setPrefixCalled) closeStartTag();
+ if(doIndent && seenTag) seenTag = false;
+ writeElementContent(buf, start, len, out);
+ return this;
+ }
+
+ public void cdsect (String text) throws IOException
+ {
+ if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
+ if(doIndent && seenTag) seenTag = false;
+ out.write("");
+ }
+
+ public void entityRef (String text) throws IOException
+ {
+ if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
+ if(doIndent && seenTag) seenTag = false;
+ out.write('&');
+ out.write(text); //escape?
+ out.write(';');
+ }
+
+ public void processingInstruction (String text) throws IOException
+ {
+ if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
+ if(doIndent && seenTag) seenTag = false;
+ out.write("");
+ out.write(text); //escape?
+ out.write("?>");
+ }
+
+ public void comment (String text) throws IOException
+ {
+ if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
+ if(doIndent && seenTag) seenTag = false;
+ out.write("");
+ }
+
+ public void docdecl (String text) throws IOException
+ {
+ if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
+ if(doIndent && seenTag) seenTag = false;
+ out.write("");
+ }
+
+ public void ignorableWhitespace (String text) throws IOException
+ {
+ if(startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
+ if(doIndent && seenTag) seenTag = false;
+ if(text.length() == 0) {
+ throw new IllegalArgumentException(
+ "empty string is not allowed for ignorable whitespace"+getLocation());
+ }
+ out.write(text); //no escape?
+ }
+
+ public void flush () throws IOException
+ {
+ if(!finished && startTagIncomplete) closeStartTag();
+ out.flush();
+ }
+
+ // --- utility methods
+
+ protected void writeAttributeValue(String value, Writer out) throws IOException
+ {
+ //.[apostrophe and <, & escaped],
+ final char quot = attributeUseApostrophe ? '\'' : '"';
+ final String quotEntity = attributeUseApostrophe ? "'" : """;
+
+ int pos = 0;
+ for (int i = 0; i < value.length(); i++)
+ {
+ char ch = value.charAt(i);
+ if(ch == '&') {
+ if(i > pos) out.write(value.substring(pos, i));
+ out.write("&");
+ pos = i + 1;
+ } if(ch == '<') {
+ if(i > pos) out.write(value.substring(pos, i));
+ out.write("<");
+ pos = i + 1;
+ }else if(ch == quot) {
+ if(i > pos) out.write(value.substring(pos, i));
+ out.write(quotEntity);
+ pos = i + 1;
+ } else if(ch < 32) {
+ //in XML 1.0 only legal character are #x9 | #xA | #xD
+ // and they must be escaped otherwise in attribute value they are normalized to spaces
+ if(ch == 13 || ch == 10 || ch == 9) {
+ if(i > pos) out.write(value.substring(pos, i));
+ out.write("");
+ out.write(Integer.toString(ch));
+ out.write(';');
+ pos = i + 1;
+ } else {
+ if(TRACE_ESCAPING) System.err.println(getClass().getName()+" DEBUG ATTR value.len="+value.length()+" "+printable(value));
+
+ throw new IllegalStateException(
+ //"character "+Integer.toString(ch)+" is not allowed in output"+getLocation());
+ "character "+printable(ch)+" ("+Integer.toString(ch)+") is not allowed in output"+getLocation()
+ +" (attr value="+printable(value)+")");
+ // in XML 1.1 legal are [#x1-#xD7FF]
+ // if(ch > 0) {
+ // if(i > pos) out.write(text.substring(pos, i));
+ // out.write("");
+ // out.write(Integer.toString(ch));
+ // out.write(';');
+ // pos = i + 1;
+ // } else {
+ // throw new IllegalStateException(
+ // "character zero is not allowed in XML 1.1 output"+getLocation());
+ // }
+ }
+ }
+ }
+ if(pos > 0) {
+ out.write(value.substring(pos));
+ } else {
+ out.write(value); // this is shortcut to the most common case
+ }
+
+ }
+
+ protected void writeElementContent(String text, Writer out) throws IOException
+ {
+ // esccape '<', '&', ']]>', <32 if necessary
+ int pos = 0;
+ for (int i = 0; i < text.length(); i++)
+ {
+ //TODO: check if doing char[] text.getChars() would be faster than getCharAt(i) ...
+ char ch = text.charAt(i);
+ if(ch == ']') {
+ if(seenBracket) {
+ seenBracketBracket = true;
+ } else {
+ seenBracket = true;
+ }
+ } else {
+ if(ch == '&') {
+ if(i > pos) out.write(text.substring(pos, i));
+ out.write("&");
+ pos = i + 1;
+ } else if(ch == '<') {
+ if(i > pos) out.write(text.substring(pos, i));
+ out.write("<");
+ pos = i + 1;
+ } else if(seenBracketBracket && ch == '>') {
+ if(i > pos) out.write(text.substring(pos, i));
+ out.write(">");
+ pos = i + 1;
+ } else if(ch < 32) {
+ //in XML 1.0 only legal character are #x9 | #xA | #xD
+ if( ch == 9 || ch == 10 || ch == 13) {
+ // pass through
+
+ // } else if(ch == 13) { //escape
+ // if(i > pos) out.write(text.substring(pos, i));
+ // out.write("");
+ // out.write(Integer.toString(ch));
+ // out.write(';');
+ // pos = i + 1;
+ } else {
+ if(TRACE_ESCAPING) System.err.println(getClass().getName()+" DEBUG TEXT value.len="+text.length()+" "+printable(text));
+ throw new IllegalStateException(
+ "character "+Integer.toString(ch)+" is not allowed in output"+getLocation()
+ +" (text value="+printable(text)+")");
+ // in XML 1.1 legal are [#x1-#xD7FF]
+ // if(ch > 0) {
+ // if(i > pos) out.write(text.substring(pos, i));
+ // out.write("");
+ // out.write(Integer.toString(ch));
+ // out.write(';');
+ // pos = i + 1;
+ // } else {
+ // throw new IllegalStateException(
+ // "character zero is not allowed in XML 1.1 output"+getLocation());
+ // }
+ }
+ }
+ if(seenBracket) {
+ seenBracketBracket = seenBracket = false;
+ }
+
+ }
+ }
+ if(pos > 0) {
+ out.write(text.substring(pos));
+ } else {
+ out.write(text); // this is shortcut to the most common case
+ }
+
+
+
+ }
+
+ protected void writeElementContent(char[] buf, int off, int len, Writer out) throws IOException
+ {
+ // esccape '<', '&', ']]>'
+ final int end = off + len;
+ int pos = off;
+ for (int i = off; i < end; i++)
+ {
+ final char ch = buf[i];
+ if(ch == ']') {
+ if(seenBracket) {
+ seenBracketBracket = true;
+ } else {
+ seenBracket = true;
+ }
+ } else {
+ if(ch == '&') {
+ if(i > pos) {
+ out.write(buf, pos, i - pos);
+ }
+ out.write("&");
+ pos = i + 1;
+ } else if(ch == '<') {
+ if(i > pos) {
+ out.write(buf, pos, i - pos);
+ }
+ out.write("<");
+ pos = i + 1;
+
+ } else if(seenBracketBracket && ch == '>') {
+ if(i > pos) {
+ out.write(buf, pos, i - pos);
+ }
+ out.write(">");
+ pos = i + 1;
+ } else if(ch < 32) {
+ //in XML 1.0 only legal character are #x9 | #xA | #xD
+ if( ch == 9 || ch == 10 || ch == 13) {
+ // pass through
+
+
+ // } else if(ch == 13 ) { //if(ch == '\r') {
+ // if(i > pos) {
+ // out.write(buf, pos, i - pos);
+ // }
+ // out.write("");
+ // out.write(Integer.toString(ch));
+ // out.write(';');
+ // pos = i + 1;
+ } else {
+ if(TRACE_ESCAPING) System.err.println(
+ getClass().getName()+" DEBUG TEXT value.len="
+ +len+" "+printable(new String(buf,off,len)));
+ throw new IllegalStateException(
+ "character "+printable(ch)+" ("+Integer.toString(ch)+") is not allowed in output"+getLocation());
+ // in XML 1.1 legal are [#x1-#xD7FF]
+ // if(ch > 0) {
+ // if(i > pos) out.write(text.substring(pos, i));
+ // out.write("");
+ // out.write(Integer.toString(ch));
+ // out.write(';');
+ // pos = i + 1;
+ // } else {
+ // throw new IllegalStateException(
+ // "character zero is not allowed in XML 1.1 output"+getLocation());
+ // }
+ }
+ }
+ if(seenBracket) {
+ seenBracketBracket = seenBracket = false;
+ }
+ // assert seenBracketBracket == seenBracket == false;
+ }
+ }
+ if(end > pos) {
+ out.write(buf, pos, end - pos);
+ }
+ }
+
+ /** simple utility method -- good for debugging */
+ protected static final String printable(String s) {
+ if(s == null) return "null";
+ StringBuffer retval = new StringBuffer(s.length() + 16);
+ retval.append("'");
+ char ch;
+ for (int i = 0; i < s.length(); i++) {
+ addPrintable(retval, s.charAt(i));
+ }
+ retval.append("'");
+ return retval.toString();
+ }
+
+ protected static final String printable(char ch) {
+ StringBuffer retval = new StringBuffer();
+ addPrintable(retval, ch);
+ return retval.toString();
+ }
+
+ private static void addPrintable(StringBuffer retval, char ch)
+ {
+ switch (ch)
+ {
+ case '\b':
+ retval.append("\\b");
+ break;
+ case '\t':
+ retval.append("\\t");
+ break;
+ case '\n':
+ retval.append("\\n");
+ break;
+ case '\f':
+ retval.append("\\f");
+ break;
+ case '\r':
+ retval.append("\\r");
+ break;
+ case '\"':
+ retval.append("\\\"");
+ break;
+ case '\'':
+ retval.append("\\\'");
+ break;
+ case '\\':
+ retval.append("\\\\");
+ break;
+ default:
+ if (ch < 0x20 || ch > 0x7e) {
+ final String ss = "0000" + Integer.toString(ch, 16);
+ retval.append("\\u" + ss.substring(ss.length() - 4, ss.length()));
+ } else {
+ retval.append(ch);
+ }
+ }
+ }
+
+}
+