/*
* Copyright (c) 2009-2013 Panxiaobo
*
* 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 pxb.android.axml;
import static pxb.android.axml.AxmlParser.RES_XML_TYPE;
import static pxb.android.axml.AxmlParser.RES_XML_RESOURCE_MAP_TYPE;
import static pxb.android.axml.AxmlParser.RES_STRING_POOL_TYPE;
import static pxb.android.axml.AxmlParser.RES_XML_END_NAMESPACE_TYPE;
import static pxb.android.axml.AxmlParser.RES_XML_END_ELEMENT_TYPE;
import static pxb.android.axml.AxmlParser.RES_XML_START_NAMESPACE_TYPE;
import static pxb.android.axml.AxmlParser.RES_XML_START_ELEMENT_TYPE;
import static pxb.android.axml.AxmlParser.RES_XML_CDATA_TYPE;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;
import pxb.android.StringItem;
import pxb.android.StringItems;
/**
* a class to write android axml
*
* @author <a href="mailto:pxb1988@gmail.com">Panxiaobo</a>
*/
public class AxmlWriter extends AxmlVisitor {
static final Comparator<Attr> ATTR_CMP = new Comparator<Attr>() {
@Override
public int compare(Attr a, Attr b) {
int x = a.resourceId - b.resourceId;
if (x == 0) {
x = a.name.data.compareTo(b.name.data);
if (x == 0) {
boolean aNsIsnull = a.ns == null;
boolean bNsIsnull = b.ns == null;
if (aNsIsnull) {
if (bNsIsnull) {
x = 0;
} else {
x = -1;
}
} else {
if (bNsIsnull) {
x = 1;
} else {
x = a.ns.data.compareTo(b.ns.data);
}
}
}
}
return x;
}
};
static class Attr {
public int index;
public StringItem name;
public StringItem ns;
public int resourceId;
public int type;
public Object value;
public StringItem raw;
public Attr(StringItem ns, StringItem name, int resourceId) {
super();
this.ns = ns;
this.name = name;
this.resourceId = resourceId;
}
public void prepare(AxmlWriter axmlWriter) {
ns = axmlWriter.updateNs(ns);
if (this.name != null) {
if (resourceId != -1) {
this.name = axmlWriter.updateWithResourceId(this.name, this.resourceId);
} else {
this.name = axmlWriter.update(this.name);
}
}
if (value instanceof StringItem) {
value = axmlWriter.update((StringItem) value);
}
if (raw != null) {
raw = axmlWriter.update(raw);
}
}
}
static class NodeImpl extends NodeVisitor {
private Set<Attr> attrs = new TreeSet<Attr>(ATTR_CMP);
private List<NodeImpl> children = new ArrayList<NodeImpl>();
private int line;
private StringItem name;
private StringItem ns;
private StringItem text;
private int textLineNumber;
Attr id;
Attr style;
Attr clz;
public NodeImpl(String ns, String name) {
super(null);
this.ns = ns == null ? null : new StringItem(ns);
this.name = name == null ? null : new StringItem(name);
}
@Override
public void attr(String ns, String name, int resourceId, int type, Object value) {
if (name == null) {
throw new RuntimeException("name can't be null");
}
Attr a = new Attr(ns == null ? null : new StringItem(ns), new StringItem(name), resourceId);
a.type = type;
if (value instanceof ValueWrapper) {
ValueWrapper valueWrapper = (ValueWrapper) value;
if (valueWrapper.raw != null) {
a.raw = new StringItem(valueWrapper.raw);
}
a.value = valueWrapper.ref;
switch (valueWrapper.type) {
case ValueWrapper.CLASS:
clz = a;
break;
case ValueWrapper.ID:
id = a;
break;
case ValueWrapper.STYLE:
style = a;
break;
}
} else if (type == TYPE_STRING) {
StringItem raw = new StringItem((String) value);
a.raw = raw;
a.value = raw;
} else {
a.raw = null;
a.value = value;
}
attrs.add(a);
}
@Override
public NodeVisitor child(String ns, String name) {
NodeImpl child = new NodeImpl(ns, name);
this.children.add(child);
return child;
}
@Override
public void end() {
}
@Override
public void line(int ln) {
this.line = ln;
}
public int prepare(AxmlWriter axmlWriter) {
ns = axmlWriter.updateNs(ns);
name = axmlWriter.update(name);
int attrIndex = 0;
for (Attr attr : attrs) {
attr.index = attrIndex++;
attr.prepare(axmlWriter);
}
text = axmlWriter.update(text);
int size = 24 + 36 + attrs.size() * 20;// 24 for end tag,36+x*20 for
// start tag
for (NodeImpl child : children) {
size += child.prepare(axmlWriter);
}
if (text != null) {
size += 28;
}
return size;
}
@Override
public void text(int ln, String value) {
this.text = new StringItem(value);
this.textLineNumber = ln;
}
void write(ByteBuffer out) throws IOException {
// start tag
out.putInt(RES_XML_START_ELEMENT_TYPE | (0x0010 << 16));
out.putInt(36 + attrs.size() * 20);
out.putInt(line);
out.putInt(0xFFFFFFFF);
out.putInt(ns != null ? this.ns.index : -1);
out.putInt(name.index);
out.putInt(0x00140014);// TODO
out.putShort((short) this.attrs.size());
out.putShort((short) (id == null ? 0 : id.index + 1));
out.putShort((short) (clz == null ? 0 : clz.index + 1));
out.putShort((short) (style == null ? 0 : style.index + 1));
for (Attr attr : attrs) {
out.putInt(attr.ns == null ? -1 : attr.ns.index);
out.putInt(attr.name.index);
out.putInt(attr.raw != null ? attr.raw.index : -1);
out.putInt((attr.type << 24) | 0x000008);
Object v = attr.value;
if (v instanceof StringItem) {
out.putInt(((StringItem) attr.value).index);
} else if (v instanceof Boolean) {
out.putInt(Boolean.TRUE.equals(v) ? -1 : 0);
} else {
out.putInt((Integer) attr.value);
}
}
if (this.text != null) {
out.putInt(RES_XML_CDATA_TYPE | (0x0010 << 16));
out.putInt(28);
out.putInt(textLineNumber);
out.putInt(0xFFFFFFFF);
out.putInt(text.index);
out.putInt(0x00000008);
out.putInt(0x00000000);
}
// children
for (NodeImpl child : children) {
child.write(out);
}
// end tag
out.putInt(RES_XML_END_ELEMENT_TYPE | (0x0010 << 16));
out.putInt(24);
out.putInt(-1);
out.putInt(0xFFFFFFFF);
out.putInt(ns != null ? this.ns.index : -1);
out.putInt(name.index);
}
}
static class Ns {
int ln;
StringItem prefix;
StringItem uri;
public Ns(StringItem prefix, StringItem uri, int ln) {
super();
this.prefix = prefix;
this.uri = uri;
this.ln = ln;
}
}
private List<NodeImpl> firsts = new ArrayList<NodeImpl>(3);
private Map<String, Ns> nses = new HashMap<String, Ns>();
private List<StringItem> otherString = new ArrayList<StringItem>();
private Map<String, StringItem> resourceId2Str = new HashMap<String, StringItem>();
private List<Integer> resourceIds = new ArrayList<Integer>();
private List<StringItem> resourceString = new ArrayList<StringItem>();
private StringItems stringItems = new StringItems();
// TODO add style support
// private List<StringItem> styleItems = new ArrayList();
@Override
public NodeVisitor child(String ns, String name) {
NodeImpl first = new NodeImpl(ns, name);
this.firsts.add(first);
return first;
}
@Override
public void end() {
}
@Override
public void ns(String prefix, String uri, int ln) {
nses.put(uri, new Ns(prefix == null ? null : new StringItem(prefix), new StringItem(uri), ln));
}
private int prepare() throws IOException {
int size = 0;
for (NodeImpl first : firsts) {
size += first.prepare(this);
}
{
int a = 0;
for (Map.Entry<String, Ns> e : nses.entrySet()) {
Ns ns = e.getValue();
if (ns == null) {
ns = new Ns(null, new StringItem(e.getKey()), 0);
e.setValue(ns);
}
if (ns.prefix == null) {
ns.prefix = new StringItem(String.format("axml_auto_%02d", a++));
}
ns.prefix = update(ns.prefix);
ns.uri = update(ns.uri);
}
}
size += nses.size() * 24 * 2;
this.stringItems.addAll(resourceString);
resourceString = null;
this.stringItems.addAll(otherString);
otherString = null;
this.stringItems.prepare();
int stringSize = this.stringItems.getSize();
if (stringSize % 4 != 0) {
stringSize += 4 - stringSize % 4;
}
size += 8 + stringSize;
size += 8 + resourceIds.size() * 4;
return size;
}
public byte[] toByteArray() throws IOException {
int size = 8 + prepare();
ByteBuffer out = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
out.putInt(RES_XML_TYPE | (0x0008 << 16));
out.putInt(size);
int stringSize = this.stringItems.getSize();
int padding = 0;
if (stringSize % 4 != 0) {
padding = 4 - stringSize % 4;
}
out.putInt(RES_STRING_POOL_TYPE | (0x001C << 16));
out.putInt(stringSize + padding + 8);
this.stringItems.write(out);
out.put(new byte[padding]);
out.putInt(RES_XML_RESOURCE_MAP_TYPE | (0x0008 << 16));
out.putInt(8 + this.resourceIds.size() * 4);
for (Integer i : resourceIds) {
out.putInt(i);
}
Stack<Ns> stack = new Stack<Ns>();
for (Map.Entry<String, Ns> e : this.nses.entrySet()) {
Ns ns = e.getValue();
stack.push(ns);
out.putInt(RES_XML_START_NAMESPACE_TYPE | (0x0010 << 16));
out.putInt(24);
out.putInt(-1);
out.putInt(0xFFFFFFFF);
out.putInt(ns.prefix.index);
out.putInt(ns.uri.index);
}
for (NodeImpl first : firsts) {
first.write(out);
}
while (stack.size() > 0) {
Ns ns = stack.pop();
out.putInt(RES_XML_END_NAMESPACE_TYPE | (0x0010 << 16));
out.putInt(24);
out.putInt(ns.ln);
out.putInt(0xFFFFFFFF);
out.putInt(ns.prefix.index);
out.putInt(ns.uri.index);
}
return out.array();
}
StringItem update(StringItem item) {
if (item == null)
return null;
int i = this.otherString.indexOf(item);
if (i < 0) {
StringItem copy = new StringItem(item.data);
this.otherString.add(copy);
return copy;
} else {
return this.otherString.get(i);
}
}
StringItem updateNs(StringItem item) {
if (item == null) {
return null;
}
String ns = item.data;
if (!this.nses.containsKey(ns)) {
this.nses.put(ns, null);
}
return update(item);
}
StringItem updateWithResourceId(StringItem name, int resourceId) {
String key = name.data + resourceId;
StringItem item = this.resourceId2Str.get(key);
if (item != null) {
return item;
} else {
StringItem copy = new StringItem(name.data);
resourceIds.add(resourceId);
resourceString.add(copy);
resourceId2Str.put(key, copy);
return copy;
}
}
}