Как получить атрибут из XMLReader

У меня есть некоторый HTML, который я конвертирую в Spanned с помощью Html.fromHtml(...), и у меня есть собственный тег, который я использую в нем:

<customtag id="1234">

Итак, я реализовал TagHandler для обработки этого пользовательского тега, например:

public void handleTag( boolean opening, String tag, Editable output, XMLReader xmlReader ) {

    if ( tag.equalsIgnoreCase( "customtag" ) ) {

        String id = xmlReader.getProperty( "id" ).toString();
    }
}

В этом случае я получаю исключение SAX, поскольку я считаю, что поле «id» на самом деле является атрибутом, а не свойством. Однако для XMLReader не существует метода getAttribute(). Итак, мой вопрос: как мне получить значение поля «id», используя этот XMLReader? Спасибо.


person Jason Robinson    schedule 05.08.2011    source источник
comment
Где TagHandler? Обычный способ сделать SAX2 — использовать ContentHandlers, не так ли?   -  person Ray Toal    schedule 05.08.2011
comment
TagHandler используется при преобразовании текста HTML в текст Spannable через Html.fromHtml(String, ImageGetter, TagHandler). Он предназначен для обработки неизвестных тегов (тегов, не распознаваемых TagSoup).   -  person Jason Robinson    schedule 05.08.2011
comment
Понимаю. Я просто пометил вопрос с помощью TagSoup, чтобы те, кто знаком с этим парсером, могли найти вопрос. Я знаю, что в обычном синтаксическом анализаторе SAX2 в стандартных библиотеках Java вы просто настраиваете ContentHandlers, а не TagHandlers, а обратный вызов startElement уже имеет атрибуты.   -  person Ray Toal    schedule 06.08.2011
comment
У меня была та же проблема, и когда я посмотрел исходный код Android, я увидел, что атрибуты намеренно не передаются. Поэтому я заменяю теги с атрибутами на другие теги с определенным именем. Как ‹customtag1234› в вашем случае.   -  person vortexwolf    schedule 06.09.2011
comment
@rekire Нет, не знал. В итоге я сделал то, что предложил vorrtex.   -  person Jason Robinson    schedule 01.03.2013
comment
Я нашел решение с отражением xmlReader. Внутри находится theElement, в котором я нашел атрибуты. Я могу опубликовать код на следующей неделе.   -  person rekire    schedule 01.03.2013
comment
@rekire Конечно, не стесняйтесь. Я попробую его, как только вы опубликуете его, и приму его, если он сработает.   -  person Jason Robinson    schedule 01.03.2013
comment
Есть ли у кого-нибудь опыт замены, например github.com/NightWhistler/HtmlSpanner или github.com/commonsguy/cwac-richedit?   -  person vitaly    schedule 13.05.2015


Ответы (5)


Вот мой код для получения частных атрибутов xmlReader путем отражения:

Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
elementField.setAccessible(true);
Object element = elementField.get(xmlReader);
Field attsField = element.getClass().getDeclaredField("theAtts");
attsField.setAccessible(true);
Object atts = attsField.get(element);
Field dataField = atts.getClass().getDeclaredField("data");
dataField.setAccessible(true);
String[] data = (String[])dataField.get(atts);
Field lengthField = atts.getClass().getDeclaredField("length");
lengthField.setAccessible(true);
int len = (Integer)lengthField.get(atts);

String myAttributeA = null;
String myAttributeB = null;

for(int i = 0; i < len; i++) {
    if("attrA".equals(data[i * 5 + 1])) {
        myAttributeA = data[i * 5 + 4];
    } else if("attrB".equals(data[i * 5 + 1])) {
        myAttributeB = data[i * 5 + 4];
    }
}

Обратите внимание, что вы можете поместить значения в карту, но для моего использования это слишком много накладных расходов.

person rekire    schedule 04.03.2013
comment
Хм, это продолжает вызывать java.lang.NoSuchFieldException - что я делаю неправильно? - person slott; 02.07.2014
comment
@slott есть три возможности: поле больше не существует (может быть, несовместимая версия); в общем, вы обращаетесь к частному полю и забыли вызвать setAccessible(true) или оно является частью базового класса, в последнем случае вам нужно проверить его суперкласс. - person rekire; 02.07.2014
comment
У меня это сработало - хитрость заключалась не в том, чтобы вызывать первым делом внутри метода handleTag :) Спасибо за помощь - всегда помогает, когда кто-то толкает вас через трудные участки жизни... - person slott; 02.07.2014

Основываясь на ответе rekire, я сделал это немного более надежное решение, которое будет обрабатывать любой тег.

private TagHandler tagHandler = new TagHandler() {
    final HashMap<String, String> attributes = new HashMap<String, String>();

    private void processAttributes(final XMLReader xmlReader) {
        try {
            Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
            elementField.setAccessible(true);
            Object element = elementField.get(xmlReader);
            Field attsField = element.getClass().getDeclaredField("theAtts");
            attsField.setAccessible(true);
            Object atts = attsField.get(element);
            Field dataField = atts.getClass().getDeclaredField("data");
            dataField.setAccessible(true);
            String[] data = (String[])dataField.get(atts);
            Field lengthField = atts.getClass().getDeclaredField("length");
            lengthField.setAccessible(true);
            int len = (Integer)lengthField.get(atts);

            /**
             * MSH: Look for supported attributes and add to hash map.
             * This is as tight as things can get :)
             * The data index is "just" where the keys and values are stored. 
             */
            for(int i = 0; i < len; i++)
                attributes.put(data[i * 5 + 1], data[i * 5 + 4]);
        }
        catch (Exception e) {
            Log.d(TAG, "Exception: " + e);
        }
    }
...

И внутри handleTag выполните:

    @Override
    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {

        processAttributes(xmlReader);
...

И тогда атрибуты будут доступны так:

attribute.get («имя моего атрибута»);

person slott    schedule 02.07.2014

Можно использовать XmlReader, предоставленный TagHandler, и получить доступ к значениям атрибутов тега без отражения, но этот метод еще менее прост, чем отражение. Хитрость заключается в том, чтобы заменить ContentHandler, используемый XmlReader, на пользовательский объект. Заменить ContentHandler можно только в вызове handleTag(). Это создает проблему получения значений атрибутов для первого тега, которую можно решить, добавив пользовательский тег в начале html.

import android.text.Editable;
import android.text.Html;
import android.text.Spanned;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import java.util.ArrayDeque;

public class HtmlParser implements Html.TagHandler, ContentHandler
{
    public interface TagHandler
    {
        boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes);
    }

    public static Spanned buildSpannedText(String html, TagHandler handler)
    {
        // add a tag at the start that is not handled by default,
        // allowing custom tag handler to replace xmlReader contentHandler
        return Html.fromHtml("<inject/>" + html, null, new HtmlParser(handler));
    }

    public static String getValue(Attributes attributes, String name)
    {
        for (int i = 0, n = attributes.getLength(); i < n; i++)
        {
            if (name.equals(attributes.getLocalName(i)))
                return attributes.getValue(i);
        }
        return null;
    }

    private final TagHandler handler;
    private ContentHandler wrapped;
    private Editable text;
    private ArrayDeque<Boolean> tagStatus = new ArrayDeque<>();

    private HtmlParser(TagHandler handler)
    {
        this.handler = handler;
    }

    @Override
    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader)
    {
        if (wrapped == null)
        {
            // record result object
            text = output;

            // record current content handler
            wrapped = xmlReader.getContentHandler();

            // replace content handler with our own that forwards to calls to original when needed
            xmlReader.setContentHandler(this);

            // handle endElement() callback for <inject/> tag
            tagStatus.addLast(Boolean.FALSE);
        }
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes)
            throws SAXException
    {
        boolean isHandled = handler.handleTag(true, localName, text, attributes);
        tagStatus.addLast(isHandled);
        if (!isHandled)
            wrapped.startElement(uri, localName, qName, attributes);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException
    {
        if (!tagStatus.removeLast())
            wrapped.endElement(uri, localName, qName);
        handler.handleTag(false, localName, text, null);
    }

    @Override
    public void setDocumentLocator(Locator locator)
    {
        wrapped.setDocumentLocator(locator);
    }

    @Override
    public void startDocument() throws SAXException
    {
        wrapped.startDocument();
    }

    @Override
    public void endDocument() throws SAXException
    {
        wrapped.endDocument();
    }

    @Override
    public void startPrefixMapping(String prefix, String uri) throws SAXException
    {
        wrapped.startPrefixMapping(prefix, uri);
    }

    @Override
    public void endPrefixMapping(String prefix) throws SAXException
    {
        wrapped.endPrefixMapping(prefix);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException
    {
        wrapped.characters(ch, start, length);
    }

    @Override
    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException
    {
        wrapped.ignorableWhitespace(ch, start, length);
    }

    @Override
    public void processingInstruction(String target, String data) throws SAXException
    {
        wrapped.processingInstruction(target, data);
    }

    @Override
    public void skippedEntity(String name) throws SAXException
    {
        wrapped.skippedEntity(name);
    }
}

С этим атрибутом чтения класса легко:

    HtmlParser.buildSpannedText("<x id=1 value=a>test<x id=2 value=b>", new HtmlParser.TagHandler()
    {
        @Override
        public boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes)
        {
            if (opening && tag.equals("x"))
            {
                String id = HtmlParser.getValue(attributes, "id");
                String value = HtmlParser.getValue(attributes, "value");
            }
            return false;
        }
    });

Преимущество этого подхода в том, что он позволяет отключать обработку одних тегов и использовать обработку по умолчанию для других, например. вы можете убедиться, что объекты ImageSpan не создаются:

    Spanned result = HtmlParser.buildSpannedText("<b><img src=nothing>test</b><img src=zilch>",
            new HtmlParser.TagHandler()
            {
                @Override
                public boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes)
                {
                    // return true here to indicate that this tag was handled and
                    // should not be processed further
                    return tag.equals("img");
                }
            });
person Juozas Kontvainis    schedule 10.04.2016
comment
Я только что попробовал это, и часть обработчика работает очень хорошо, однако встроенный анализатор как-то испорчен, <tag attr="a & b">text</tag> анализируется как attributes = {attr="a", _="_", b="b"}. Использование &amp; также плохо. - person TWiStErRob; 21.07.2016
comment
@TWiStErRob для меня String testStr = "<tag attr=\"a & b\">text</tag>"; анализируется правильно. Я получаю проблему, которую вы описали, анализируя String testStr = "<tag attr=a & b>text</tag>";, который на самом деле не является допустимым html. - person Juozas Kontvainis; 24.07.2016
comment
Хм, вы используете жестко закодированную Java, я прочитал значение из ресурсов: <string name="..."><![CDATA[ ... <tag...> ... ]]></string>. Возможно, что-то потерялось при передаче, пока фреймворк читал XML. Я работал над этим, используя идентификаторы Java (например, имя константы перечисления) вместо простого текста в атрибутах, поэтому они состоят всего из одного слова. - person TWiStErRob; 24.07.2016

Есть альтернатива другим решениям, которая не позволяет использовать пользовательские теги, но имеет тот же эффект:

<string name="foobar">blah <annotation customTag="1234">inside blah</annotation> more blah</string>

Тогда прочитайте это так:

CharSequence annotatedText = context.getText(R.string.foobar);
// wrap, because getText returns a SpannedString, which is not mutable
CharSequence processedText = replaceCustomTags(new SpannableStringBuilder(annotatedText));

public static <T extends Spannable> T replaceCustomTags(T text) {
    Annotation[] annotations = text.getSpans(0, text.length(), Annotation.class);
    for (Annotation a : annotations) {
        String attrName = a.getKey();
        if ("customTag".equals(attrName)) {
            String attrValue = a.getValue();
            int contentStart = text.getSpanStart(a);
            int contentEnd = text.getSpanEnd(a);
            int contentFlags = text.getSpanFlags(a);
            Object newFormat1 = new StyleSpan(Typeface.BOLD);
            Object newFormat2 = new ForegroundColorSpan(Color.RED);
            text.setSpan(newFormat1, contentStart, contentEnd, contentFlags);
            text.setSpan(newFormat2, contentStart, contentEnd, contentFlags);
            text.removeSpan(a);
        }
    }
    return text;
}

В зависимости от того, что вы хотели сделать со своими пользовательскими тегами, приведенное выше может вам помочь. Если вы просто хотите их прочитать, вам не нужен SpannableStringBuilder, просто приведите интерфейс getText к Spanned для изучения.

Обратите внимание, что Annotation, представляющий <annotation foo="bar">...</annotation>, является встроенным в Android, начиная с уровня API 1! Это снова одна из тех скрытых жемчужин. У It есть ограничение в один атрибут на тег <annotation>, но ничто не мешает вам вкладывать несколько аннотаций для получения нескольких атрибутов:

<string name="gold_admin_user"><annotation user="admin"><annotation rank="gold">$$username$$</annotation></annotation></string>

Если вы используете интерфейс Editable вместо Spannable, вы также можете изменить содержимое вокруг каждой аннотации. Например, изменив приведенный выше код:

String attrValue = a.getValue();
text.insert(text.getSpanStart(a), attrValue);
text.insert(text.getSpanStart(a) + attrValue.length(), " ");
int contentStart = text.getSpanStart(a);

получится так, как если бы у вас было это в XML:

blah <b><font color="#ff0000">1234 inside blah</font></b> more blah

Одно предостережение, на которое следует обратить внимание: когда вы вносите изменения, влияющие на длину текста, интервалы перемещаются. Убедитесь, что вы читаете начальные/конечные индексы диапазона в правильное время, лучше всего, если вы встраиваете их в вызовы методов.

Editable также позволяет выполнять простой поиск и замену замены:

index = TextUtils.indexOf(text, needle); // for example $$username$$ above
text.replace(index, index + needle.length(), replacement);
person TWiStErRob    schedule 29.07.2016

Если все, что вам нужно, это всего лишь один атрибут, предложение vorrtex на самом деле довольно солидно. Чтобы дать вам пример того, насколько просто было бы справиться, посмотрите здесь:

<xml>Click on <user1>Johnni<user1> or <user2>Jenny<user2> to see...</<xml>

И в вашем пользовательском TagHandler вы используете не equals, а indexOf

final static String USER = "user";
if(tag.indexOf(USER) == 0) {
    // Extract tag postfix.
    String postfix = tag.substring(USER.length());
    Log.d(TAG, "postfix: " + postfix);
}

Затем вы можете передать значение постфикса в параметре представления onClick в качестве тега, чтобы он оставался универсальным.

person slott    schedule 02.07.2014