package org.itsnat.droid.impl.domparser;
import android.content.Context;
import android.content.res.AssetManager;
import android.util.Xml;
import org.itsnat.droid.ItsNatDroidException;
import org.itsnat.droid.impl.browser.HttpRequestResultOKImpl;
import org.itsnat.droid.impl.dom.DOMAttr;
import org.itsnat.droid.impl.dom.DOMAttrLocal;
import org.itsnat.droid.impl.dom.DOMAttrRemote;
import org.itsnat.droid.impl.dom.DOMElement;
import org.itsnat.droid.impl.dom.ParsedResource;
import org.itsnat.droid.impl.dom.ParsedResourceImage;
import org.itsnat.droid.impl.dom.ParsedResourceXMLDOM;
import org.itsnat.droid.impl.dom.ResourceDescAsset;
import org.itsnat.droid.impl.dom.ResourceDescDynamic;
import org.itsnat.droid.impl.dom.ResourceDescIntern;
import org.itsnat.droid.impl.dom.ResourceDescLocal;
import org.itsnat.droid.impl.dom.ResourceDescRemote;
import org.itsnat.droid.impl.dom.XMLDOM;
import org.itsnat.droid.impl.dom.values.XMLDOMValues;
import org.itsnat.droid.impl.domparser.animinterp.XMLDOMInterpolatorParser;
import org.itsnat.droid.impl.domparser.animlayout.XMLDOMLayoutAnimationParser;
import org.itsnat.droid.impl.domparser.layout.XMLDOMLayoutParser;
import org.itsnat.droid.impl.util.IOUtil;
import org.itsnat.droid.impl.util.MimeUtil;
import org.itsnat.droid.impl.util.MiscUtil;
import org.itsnat.droid.impl.util.NamespaceUtil;
import org.itsnat.droid.impl.util.StringUtil;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
/**
* Created by jmarranz on 31/10/14.
*/
public abstract class XMLDOMParser<Txmldom extends XMLDOM>
{
static final String internLocationBase = "intern";
protected final XMLDOMParserContext xmlDOMParserContext;
public XMLDOMParser(XMLDOMParserContext xmlDOMParserContext)
{
this.xmlDOMParserContext = xmlDOMParserContext;
}
private static XmlPullParser newPullParser(Reader input)
{
try
{
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
parser.setInput(input);
return parser;
}
catch (XmlPullParserException ex)
{
throw new ItsNatDroidException(ex);
}
}
public void parse(String markup,Txmldom xmlDOM)
{
StringReader input = new StringReader(markup);
parse(input,xmlDOM);
}
private void parse(Reader input,Txmldom xmlDOM)
{
try
{
XmlPullParser parser = newPullParser(input);
parse(parser,xmlDOM);
}
catch (IOException ex) { throw new ItsNatDroidException(ex); }
catch (XmlPullParserException ex) { throw new ItsNatDroidException(ex); }
finally
{
try { input.close(); }
catch (IOException ex) { throw new ItsNatDroidException(ex); }
}
}
private void parse(XmlPullParser parser,Txmldom xmlDOM) throws IOException, XmlPullParserException
{
String rootElemName = getRootElementName(parser);
parseRootElement(rootElemName,parser, xmlDOM);
}
public DOMElement parseRootElement(String rootElemName, XmlPullParser parser, XMLDOM xmlDOM) throws IOException, XmlPullParserException
{
int nsStart = parser.getNamespaceCount(parser.getDepth() - 1);
int nsEnd = parser.getNamespaceCount(parser.getDepth());
for (int i = nsStart; i < nsEnd; i++)
{
String prefix = parser.getNamespacePrefix(i);
String ns = parser.getNamespaceUri(i);
xmlDOM.addNamespace(prefix, ns);
}
if (isAndroidNSPrefixNeeded() && xmlDOM.getAndroidNSPrefix() == null)
throw new ItsNatDroidException("Missing android namespace declaration in the root element of the XML");
DOMElement rootElement = createRootElementAndFillAttributes(rootElemName, parser, xmlDOM);
xmlDOM.setRootElement(rootElement);
processChildElements(rootElement, parser, xmlDOM);
return rootElement;
}
protected abstract boolean isAndroidNSPrefixNeeded();
protected DOMElement createRootElementAndFillAttributes(String name,XmlPullParser parser,XMLDOM xmlDOM) throws IOException, XmlPullParserException
{
DOMElement rootElement = createElement(name, null);
fillAttributesAndAddElement(null, rootElement, parser, xmlDOM);
return rootElement;
}
protected DOMElement createElementAndFillAttributesAndAdd(String name, DOMElement parentElement, XmlPullParser parser,XMLDOM xmlDOM) throws XmlPullParserException
{
// parentElement es null en el caso de parseo de fragment
DOMElement element = createElement(name,parentElement);
fillAttributesAndAddElement(parentElement, element, parser, xmlDOM);
return element;
}
protected void fillAttributesAndAddElement(DOMElement parentElement, DOMElement element,XmlPullParser parser,XMLDOM xmlDOM) throws XmlPullParserException
{
fillElementAttributes(element, parser, xmlDOM);
if (parentElement != null) parentElement.addChildDOMElement(element);
}
protected void fillElementAttributes(DOMElement element,XmlPullParser parser,XMLDOM xmlDOM) throws XmlPullParserException
{
if (element.getParentDOMElement() != null) // No es root
{
int depth = parser.getDepth();
int nsStart = parser.getNamespaceCount(depth - 1);
int nsEnd = parser.getNamespaceCount(depth);
if (nsStart != nsEnd)
{
int nscount = 0;
for (int i = nsStart; i < nsEnd; i++)
{
String prefix = parser.getNamespacePrefix(i);
String ns = parser.getNamespaceUri(i);
if ("android".equals(prefix) && NamespaceUtil.XMLNS_ANDROID.equals(ns)) continue; // En el caso de <merge> es posible que acabemos poniendo el namespace android anidado, lo toleramos para no complicarnos la vida
nscount++;
}
if (nscount > 0)
throw new ItsNatDroidException("Namespaces different to android namespace must only be defined in root element");
}
}
int len = parser.getAttributeCount();
if (len == 0) return;
element.initDOMAttribMap(len);
for (int i = 0; i < len; i++)
{
String namespaceURI = parser.getAttributeNamespace(i);
if ("".equals(namespaceURI)) namespaceURI = null; // Por estandarizar
String name = parser.getAttributeName(i); // El nombre devuelto no contiene el namespace
String value = parser.getAttributeValue(i);
addDOMAttr(element, namespaceURI, name, value, xmlDOM);
}
}
protected void processChildElements(DOMElement parentElement,XmlPullParser parser,XMLDOM xmlDOM) throws IOException, XmlPullParserException
{
DOMElement childView = parseNextChild(parentElement, parser, xmlDOM);
while (childView != null)
{
childView = parseNextChild(parentElement,parser, xmlDOM);
}
}
private DOMElement parseNextChild(DOMElement parentElement,XmlPullParser parser,XMLDOM xmlDOM) throws IOException, XmlPullParserException
{
while (parser.next() != XmlPullParser.END_TAG)
{
if (parser.getEventType() != XmlPullParser.START_TAG) // Nodo de texto etc
continue;
String name = parser.getName(); // viewName lo normal es que sea un nombre corto por ej RelativeLayout
DOMElement element = processElement(name, parentElement, parser, xmlDOM);
if (element == null) continue; // Se ignora
return element;
}
return null;
}
protected DOMElement processElement(String name, DOMElement parentElement, XmlPullParser parser,XMLDOM xmlDOM) throws IOException, XmlPullParserException
{
DOMElement element = createElementAndFillAttributesAndAdd(name, parentElement, parser, xmlDOM);
processChildElements(element,parser, xmlDOM);
return element;
}
protected static String findAttributeFromParser(String namespaceURI, String name, XmlPullParser parser)
{
for(int i = 0; i < parser.getAttributeCount(); i++)
{
String currNamespaceURI = parser.getAttributeNamespace(i);
if ("".equals(currNamespaceURI)) currNamespaceURI = null; // Por estandarizar
if (!MiscUtil.equalsNullAllowed(currNamespaceURI, namespaceURI)) continue;
String currName = parser.getAttributeName(i); // El nombre devuelto no contiene el namespace
if (!name.equals(currName)) continue;
String value = parser.getAttributeValue(i);
return value;
}
return null;
}
protected static String getRootElementName(XmlPullParser parser) throws IOException, XmlPullParserException
{
while (parser.next() != XmlPullParser.END_TAG)
{
if (parser.getEventType() != XmlPullParser.START_TAG) // Nodo de texto etc
continue;
return parser.getName();
}
throw new ItsNatDroidException("INTERNAL ERROR: NO ROOT ELEMENT");
}
protected DOMAttr addDOMAttr(DOMElement element, String namespaceURI, String name, String value, XMLDOM xmlDOMParent)
{
DOMAttr attrib = DOMAttr.createDOMAttr(namespaceURI, name, value);
addDOMAttr(element,attrib,xmlDOMParent);
return attrib;
}
protected void addDOMAttr(DOMElement element, DOMAttr attrib, XMLDOM xmlDOMParent)
{
prepareDOMAttrToLoadResource(attrib, xmlDOMParent);
element.setDOMAttribute(attrib);
}
protected void prepareDOMAttrToLoadResource(DOMAttr attrib, XMLDOM xmlDOMParent)
{
if (attrib instanceof DOMAttrRemote)
{
xmlDOMParent.addDOMAttrRemote((DOMAttrRemote) attrib);
}
else if (attrib instanceof DOMAttrLocal)
{
DOMAttrLocal localAttr = (DOMAttrLocal)attrib;
prepareResourceDescLocalToLoadResource(localAttr.getResourceDescLocal());
}
// Nada que preparar
}
public void prepareResourceDescLocalToLoadResource(ResourceDescLocal resourceDescLocal)
{
prepareResourceDescLocalToLoadResource(resourceDescLocal,xmlDOMParserContext);
}
public static void prepareResourceDescLocalToLoadResource(ResourceDescLocal resourceDescLocal,XMLDOMParserContext xmlDOMParserContext)
{
String location = resourceDescLocal.getLocation(xmlDOMParserContext); // Los assets son para pruebas, no merece la pena perder el tiempo intentando usar un "basePath" para poder especificar paths relativos
byte[] res = null;
InputStream ims = null;
try
{
if (resourceDescLocal instanceof ResourceDescAsset)
{
// En assets el location empezará siempre con res/, AssetManager.open() NO admite el uso de ".." o /res . No pasa nada, los assets son para pruebas no es necesario que se comporte igual que en remoto (con HTTP)
// AssetManager.open es multihilo, de todas formas va a ser MUY raro que usemos assets junto a remote
// http://www.netmite.com/android/mydroid/frameworks/base/libs/utils/AssetManager.cpp
AssetManager assetManager = xmlDOMParserContext.getAssetManager();
ims = assetManager.open(location);
}
else if (resourceDescLocal instanceof ResourceDescIntern)
{
Context ctxToOpenInternFiles = xmlDOMParserContext.getContextToOpenInternFiles();
File rootDir = ctxToOpenInternFiles.getDir(internLocationBase,Context.MODE_PRIVATE);
File locationFile = new File(rootDir.getAbsolutePath(),location);
ims = new FileInputStream(locationFile);
}
res = IOUtil.read(ims);
}
catch (IOException ex)
{
throw new ItsNatDroidException(ex);
}
finally
{
if (ims != null)
try { ims.close(); }
catch (IOException ex) { throw new ItsNatDroidException(ex); }
}
parseResourceDescLocal(resourceDescLocal,xmlDOMParserContext, res);
}
private static ParsedResource parseResourceDescLocal(ResourceDescLocal resourceDescLocal,XMLDOMParserContext xmlDOMParserContext, byte[] input)
{
String resourceMime = resourceDescLocal.getResourceMime();
if (MimeUtil.isMIMEResourceXML(resourceMime))
{
String markup = StringUtil.toString(input, "UTF-8");
ParsedResourceXMLDOM resource = parseResourceDescDynamicXML(markup, resourceDescLocal, null, XMLDOMLayoutParser.LayoutType.STANDALONE, xmlDOMParserContext);
XMLDOM xmlDOM = resource.getXMLDOM();
if (xmlDOM.getDOMAttrRemoteList() != null)
throw new ItsNatDroidException("Remote resources cannot be specified by a resource loaded as asset");
return resource;
}
else if (MimeUtil.isMIMEResourceImage(resourceMime))
{
ParsedResourceImage resource = parseResourceDescDynamicResourceImage(resourceDescLocal, input);
return resource;
}
else throw new ItsNatDroidException("Unsupported resource mime: " + resourceMime);
}
public static ParsedResource parseResourceDescRemote(ResourceDescRemote resourceDescRemote, HttpRequestResultOKImpl resultRes, XMLDOMParserContext xmlDOMParserContext) throws Exception
{
// Método llamado en multihilo
// No te preocupes, quizás lo hemos pre-loaded y cacheado via otro DOMAttrRemote idéntico en datos pero diferente instancia, tal es el caso del
// pre-parseo de código beanshell, es normal que no esté en este DOMAttrRemote obtenido
String resourceMime = resourceDescRemote.getResourceMime();
if (MimeUtil.isMIMEResourceXML(resourceMime))
{
String markup = resultRes.getResponseText();
String itsNatServerVersion = resultRes.getItsNatServerVersion(); // Puede ser null
ParsedResourceXMLDOM resource = parseResourceDescDynamicXML(markup, resourceDescRemote, itsNatServerVersion, XMLDOMLayoutParser.LayoutType.PAGE, xmlDOMParserContext);
return resource;
}
else if (MimeUtil.isMIMEResourceImage(resourceMime))
{
byte[] img = resultRes.getResponseByteArray();
ParsedResourceImage resource = parseResourceDescDynamicResourceImage(resourceDescRemote, img);
return resource;
}
else throw new ItsNatDroidException("Unsupported resource mime: " + resourceMime);
}
private static ParsedResourceXMLDOM<? extends XMLDOM> parseResourceDescDynamicXML(String markup,ResourceDescDynamic resourceDesc, String itsNatServerVersion, XMLDOMLayoutParser.LayoutType layoutType, XMLDOMParserContext xmlDOMParserContext)
{
XMLDOMRegistry xmlDOMRegistry = xmlDOMParserContext.getXMLDOMRegistry();
// Es llamado en multihilo en el caso de DOMAttrRemote
String resourceType = resourceDesc.getResourceType();
ParsedResourceXMLDOM<? extends XMLDOM> resource;
if (resourceDesc.getValuesResourceName() == null) // No es <drawable> o un <item name="..." type="layout"> <item ... type="anim"> o <item ... type="animator"> en un res/values/archivo.xml
{
if (XMLDOMValues.TYPE_ANIM.equals(resourceType))
{
String rootElemName = new XMLDOMAnimDiscriminatorParser().parse(markup);
if (XMLDOMLayoutAnimationParser.isLayoutAnimatorRoot(rootElemName)) // Derivados de LayoutAnimatorController
resource = xmlDOMRegistry.buildXMLDOMLayoutAnimationAndCachingByMarkupAndResDesc(markup, resourceDesc, xmlDOMParserContext);
else if (XMLDOMInterpolatorParser.isInterpolatorRoot(rootElemName)) // Derivados de Interpolator
resource = xmlDOMRegistry.buildXMLDOMInterpolatorAndCachingByMarkupAndResDesc(markup, resourceDesc, xmlDOMParserContext);
else // los derivados de Animation
resource = xmlDOMRegistry.buildXMLDOMAnimationAndCachingByMarkupAndResDesc(markup, resourceDesc, xmlDOMParserContext);
}
else if (XMLDOMValues.TYPE_ANIMATOR.equals(resourceType))
{
resource = xmlDOMRegistry.buildXMLDOMAnimatorAndCachingByMarkupAndResDesc(markup, resourceDesc, xmlDOMParserContext);
}
else if (XMLDOMValues.TYPE_DRAWABLE.equals(resourceType))
{
resource = xmlDOMRegistry.buildXMLDOMDrawableAndCachingByMarkupAndResDesc(markup,resourceDesc, xmlDOMParserContext);
}
else if (XMLDOMValues.TYPE_LAYOUT.equals(resourceType))
{
resource = xmlDOMRegistry.buildXMLDOMLayoutAndCachingByMarkupAndResDesc(markup,resourceDesc,itsNatServerVersion, layoutType, xmlDOMParserContext);
}
else throw new ItsNatDroidException("Unsupported resource type as asset or remote: " + resourceType + " or missing ending :selector");
}
else if (XMLDOMValues.isResourceTypeValues(resourceType)) // Incluye el caso de <drawable>, <item name="..." type="layout"> <item ... type="anim"> y <item ... type="animator">
{
// En el caso "drawable" podemos tener un acceso a un <drawable> en archivo XML en /res/values o bien directamente acceder al XML en /res/drawable
// Este es el caso de acceso a un item <drawable> de un XML values
// Idem con <item name="..." type="layout">
resource = xmlDOMRegistry.buildXMLDOMValuesAndCachingByMarkupAndResDesc(markup,resourceDesc, xmlDOMParserContext);
}
else throw new ItsNatDroidException("Unsupported resource type as asset or remote: " + resourceType);
return resource;
}
public static ParsedResourceImage parseResourceDescDynamicResourceImage(ResourceDescDynamic resourceDesc, byte[] img)
{
ParsedResourceImage resource = new ParsedResourceImage(img);
resourceDesc.setParsedResource(resource);
return resource;
}
protected abstract DOMElement createElement(String name,DOMElement parent);
}