1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.layoutlib.bridge.impl;
18
19import org.xmlpull.v1.XmlPullParser;
20import org.xmlpull.v1.XmlPullParserException;
21
22import android.annotation.Nullable;
23
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.Reader;
27import java.util.ArrayList;
28import java.util.Collections;
29import java.util.List;
30
31/**
32 * A wrapper around XmlPullParser that can peek forward to inspect if the file is a data-binding
33 * layout and some parts need to be stripped.
34 */
35public class LayoutParserWrapper implements XmlPullParser {
36
37    // Data binding constants.
38    private static final String TAG_LAYOUT = "layout";
39    private static final String TAG_DATA = "data";
40    private static final String DEFAULT = "default=";
41
42    private final XmlPullParser mDelegate;
43
44    // Storage for peeked values.
45    private boolean mPeeked;
46    private int mEventType;
47    private int mDepth;
48    private int mNext;
49    private List<Attribute> mAttributes;
50    private String mText;
51    private String mName;
52
53    // Used to end the document before the actual parser ends.
54    private int mFinalDepth = -1;
55    private boolean mEndNow;
56
57    public LayoutParserWrapper(XmlPullParser delegate) {
58        mDelegate = delegate;
59    }
60
61    public LayoutParserWrapper peekTillLayoutStart() throws IOException, XmlPullParserException {
62        final int STATE_LAYOUT_NOT_STARTED = 0;  // <layout> tag not encountered yet.
63        final int STATE_ROOT_NOT_STARTED = 1;    // the main view root not found yet.
64        final int STATE_INSIDE_DATA = 2;         // START_TAG for <data> found, but not END_TAG.
65
66        int state = STATE_LAYOUT_NOT_STARTED;
67        int dataDepth = -1;    // depth of the <data> tag. Should be two.
68        while (true) {
69            int peekNext = peekNext();
70            switch (peekNext) {
71                case START_TAG:
72                    if (state == STATE_LAYOUT_NOT_STARTED) {
73                        if (mName.equals(TAG_LAYOUT)) {
74                            state = STATE_ROOT_NOT_STARTED;
75                        } else {
76                            return this; // no layout tag in the file.
77                        }
78                    } else if (state == STATE_ROOT_NOT_STARTED) {
79                        if (mName.equals(TAG_DATA)) {
80                            state = STATE_INSIDE_DATA;
81                            dataDepth = mDepth;
82                        } else {
83                            mFinalDepth = mDepth;
84                            return this;
85                        }
86                    }
87                    break;
88                case END_TAG:
89                    if (state == STATE_INSIDE_DATA) {
90                        if (mDepth <= dataDepth) {
91                            state = STATE_ROOT_NOT_STARTED;
92                        }
93                    }
94                    break;
95                case END_DOCUMENT:
96                    // No layout start found.
97                    return this;
98            }
99            // consume the peeked tag.
100            next();
101        }
102    }
103
104    private int peekNext() throws IOException, XmlPullParserException {
105        if (mPeeked) {
106            return mNext;
107        }
108        mEventType = mDelegate.getEventType();
109        mNext = mDelegate.next();
110        if (mEventType == START_TAG) {
111            int count = mDelegate.getAttributeCount();
112            mAttributes = count > 0 ? new ArrayList<Attribute>(count) :
113                    Collections.<Attribute>emptyList();
114            for (int i = 0; i < count; i++) {
115                mAttributes.add(new Attribute(mDelegate.getAttributeNamespace(i),
116                        mDelegate.getAttributeName(i), mDelegate.getAttributeValue(i)));
117            }
118        }
119        mDepth = mDelegate.getDepth();
120        mText = mDelegate.getText();
121        mName = mDelegate.getName();
122        mPeeked = true;
123        return mNext;
124    }
125
126    private void reset() {
127        mAttributes = null;
128        mText = null;
129        mName = null;
130        mPeeked = false;
131    }
132
133    @Override
134    public int next() throws XmlPullParserException, IOException {
135        int returnValue;
136        int depth;
137        if (mPeeked) {
138            returnValue = mNext;
139            depth = mDepth;
140            reset();
141        } else if (mEndNow) {
142            return END_DOCUMENT;
143        } else {
144            returnValue = mDelegate.next();
145            depth = getDepth();
146        }
147        if (returnValue == END_TAG && depth <= mFinalDepth) {
148            mEndNow = true;
149        }
150        return returnValue;
151    }
152
153    @Override
154    public int getEventType() throws XmlPullParserException {
155        return mPeeked ? mEventType : mDelegate.getEventType();
156    }
157
158    @Override
159    public int getDepth() {
160        return mPeeked ? mDepth : mDelegate.getDepth();
161    }
162
163    @Override
164    public String getName() {
165        return mPeeked ? mName : mDelegate.getName();
166    }
167
168    @Override
169    public String getText() {
170        return mPeeked ? mText : mDelegate.getText();
171    }
172
173    @Override
174    public String getAttributeValue(@Nullable String namespace, String name) {
175        String returnValue = null;
176        if (mPeeked) {
177            if (mAttributes == null) {
178                if (mEventType != START_TAG) {
179                    throw new IndexOutOfBoundsException("getAttributeValue() called when not at START_TAG.");
180                } else {
181                    return null;
182                }
183            } else {
184                for (Attribute attribute : mAttributes) {
185                    //noinspection StringEquality for nullness check.
186                    if (attribute.name.equals(name) && (attribute.namespace == namespace ||
187                            attribute.namespace != null && attribute.namespace.equals(namespace))) {
188                        returnValue = attribute.value;
189                        break;
190                    }
191                }
192            }
193        } else {
194            returnValue = mDelegate.getAttributeValue(namespace, name);
195        }
196        // Check if the value is bound via data-binding, if yes get the default value.
197        if (returnValue != null && mFinalDepth >= 0 && returnValue.startsWith("@{")) {
198            // TODO: Improve the detection of default keyword.
199            int i = returnValue.lastIndexOf(DEFAULT);
200            return i > 0 ? returnValue.substring(i + DEFAULT.length(), returnValue.length() - 1)
201                    : null;
202        }
203        return returnValue;
204    }
205
206    private static class Attribute {
207        @Nullable
208        public final String namespace;
209        public final String name;
210        public final String value;
211
212        public Attribute(@Nullable String namespace, String name, String value) {
213            this.namespace = namespace;
214            this.name = name;
215            this.value = value;
216        }
217    }
218
219    // Not affected by peeking.
220
221    @Override
222    public void setFeature(String s, boolean b) throws XmlPullParserException {
223        mDelegate.setFeature(s, b);
224    }
225
226    @Override
227    public void setProperty(String s, Object o) throws XmlPullParserException {
228        mDelegate.setProperty(s, o);
229    }
230
231    @Override
232    public void setInput(InputStream inputStream, String s) throws XmlPullParserException {
233        mDelegate.setInput(inputStream, s);
234    }
235
236    @Override
237    public void setInput(Reader reader) throws XmlPullParserException {
238        mDelegate.setInput(reader);
239    }
240
241    @Override
242    public String getInputEncoding() {
243        return mDelegate.getInputEncoding();
244    }
245
246    @Override
247    public String getNamespace(String s) {
248        return mDelegate.getNamespace(s);
249    }
250
251    @Override
252    public String getPositionDescription() {
253        return mDelegate.getPositionDescription();
254    }
255
256    @Override
257    public int getLineNumber() {
258        return mDelegate.getLineNumber();
259    }
260
261    @Override
262    public String getNamespace() {
263        return mDelegate.getNamespace();
264    }
265
266    @Override
267    public int getColumnNumber() {
268        return mDelegate.getColumnNumber();
269    }
270
271    // -- We don't care much about the methods that follow.
272
273    @Override
274    public void require(int i, String s, String s1) throws XmlPullParserException, IOException {
275        throw new UnsupportedOperationException("Only few parser methods are supported.");
276    }
277
278    @Override
279    public boolean getFeature(String s) {
280        throw new UnsupportedOperationException("Only few parser methods are supported.");
281    }
282
283    @Override
284    public void defineEntityReplacementText(String s, String s1) throws XmlPullParserException {
285        throw new UnsupportedOperationException("Only few parser methods are supported.");
286    }
287
288    @Override
289    public Object getProperty(String s) {
290        throw new UnsupportedOperationException("Only few parser methods are supported.");
291    }
292
293    @Override
294    public int nextToken() throws XmlPullParserException, IOException {
295        throw new UnsupportedOperationException("Only few parser methods are supported.");
296    }
297
298    @Override
299    public int getNamespaceCount(int i) throws XmlPullParserException {
300        throw new UnsupportedOperationException("Only few parser methods are supported.");
301    }
302
303    @Override
304    public String getNamespacePrefix(int i) throws XmlPullParserException {
305        throw new UnsupportedOperationException("Only few parser methods are supported.");
306    }
307
308    @Override
309    public String getNamespaceUri(int i) throws XmlPullParserException {
310        throw new UnsupportedOperationException("Only few parser methods are supported.");
311    }
312
313    @Override
314    public boolean isWhitespace() throws XmlPullParserException {
315        throw new UnsupportedOperationException("Only few parser methods are supported.");
316    }
317
318    @Override
319    public char[] getTextCharacters(int[] ints) {
320        throw new UnsupportedOperationException("Only few parser methods are supported.");
321    }
322
323    @Override
324    public String getPrefix() {
325        throw new UnsupportedOperationException("Only few parser methods are supported.");
326    }
327
328    @Override
329    public boolean isEmptyElementTag() throws XmlPullParserException {
330        throw new UnsupportedOperationException("Only few parser methods are supported.");
331    }
332
333    @Override
334    public int getAttributeCount() {
335        throw new UnsupportedOperationException("Only few parser methods are supported.");
336    }
337
338    @Override
339    public String getAttributeNamespace(int i) {
340        throw new UnsupportedOperationException("Only few parser methods are supported.");
341    }
342
343    @Override
344    public String getAttributeName(int i) {
345        throw new UnsupportedOperationException("Only few parser methods are supported.");
346    }
347
348    @Override
349    public String getAttributePrefix(int i) {
350        throw new UnsupportedOperationException("Only few parser methods are supported.");
351    }
352
353    @Override
354    public String getAttributeType(int i) {
355        throw new UnsupportedOperationException("Only few parser methods are supported.");
356    }
357
358    @Override
359    public boolean isAttributeDefault(int i) {
360        throw new UnsupportedOperationException("Only few parser methods are supported.");
361    }
362
363    @Override
364    public String getAttributeValue(int i) {
365        throw new UnsupportedOperationException("Only few parser methods are supported.");
366    }
367
368    @Override
369    public String nextText() throws XmlPullParserException, IOException {
370        throw new UnsupportedOperationException("Only few parser methods are supported.");
371    }
372
373    @Override
374    public int nextTag() throws XmlPullParserException, IOException {
375        throw new UnsupportedOperationException("Only few parser methods are supported.");
376    }
377}
378