1/*
2 * Copyright (C) 2008 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.ide.common.resources;
18
19import com.android.ide.common.rendering.api.AttrResourceValue;
20import com.android.ide.common.rendering.api.DeclareStyleableResourceValue;
21import com.android.ide.common.rendering.api.ResourceValue;
22import com.android.ide.common.rendering.api.StyleResourceValue;
23import com.android.resources.ResourceType;
24
25import org.xml.sax.Attributes;
26import org.xml.sax.SAXException;
27import org.xml.sax.helpers.DefaultHandler;
28
29/**
30 * SAX handler to parser value resource files.
31 */
32public final class ValueResourceParser extends DefaultHandler {
33
34    // TODO: reuse definitions from somewhere else.
35    private final static String NODE_RESOURCES = "resources";
36    private final static String NODE_ITEM = "item";
37    private final static String ATTR_NAME = "name";
38    private final static String ATTR_TYPE = "type";
39    private final static String ATTR_PARENT = "parent";
40    private final static String ATTR_VALUE = "value";
41
42    private final static String DEFAULT_NS_PREFIX = "android:";
43    private final static int DEFAULT_NS_PREFIX_LEN = DEFAULT_NS_PREFIX.length();
44
45    public interface IValueResourceRepository {
46        void addResourceValue(ResourceValue value);
47        boolean hasResourceValue(ResourceType type, String name);
48    }
49
50    private boolean inResources = false;
51    private int mDepth = 0;
52    private ResourceValue mCurrentValue = null;
53    private StyleResourceValue mCurrentStyle = null;
54    private DeclareStyleableResourceValue mCurrentDeclareStyleable = null;
55    private AttrResourceValue mCurrentAttr;
56    private IValueResourceRepository mRepository;
57    private final boolean mIsFramework;
58
59    public ValueResourceParser(IValueResourceRepository repository, boolean isFramework) {
60        mRepository = repository;
61        mIsFramework = isFramework;
62    }
63
64    @Override
65    public void endElement(String uri, String localName, String qName) throws SAXException {
66        if (mCurrentValue != null) {
67            mCurrentValue.setValue(trimXmlWhitespaces(mCurrentValue.getValue()));
68        }
69
70        if (inResources && qName.equals(NODE_RESOURCES)) {
71            inResources = false;
72        } else if (mDepth == 2) {
73            mCurrentValue = null;
74            mCurrentStyle = null;
75            mCurrentDeclareStyleable = null;
76            mCurrentAttr = null;
77        } else if (mDepth == 3) {
78            mCurrentValue = null;
79            if (mCurrentDeclareStyleable != null) {
80                mCurrentAttr = null;
81            }
82        }
83
84        mDepth--;
85        super.endElement(uri, localName, qName);
86    }
87
88    @Override
89    public void startElement(String uri, String localName, String qName, Attributes attributes)
90            throws SAXException {
91        try {
92            mDepth++;
93            if (inResources == false && mDepth == 1) {
94                if (qName.equals(NODE_RESOURCES)) {
95                    inResources = true;
96                }
97            } else if (mDepth == 2 && inResources == true) {
98                ResourceType type = getType(qName, attributes);
99
100                if (type != null) {
101                    // get the resource name
102                    String name = attributes.getValue(ATTR_NAME);
103                    if (name != null) {
104                        switch (type) {
105                            case STYLE:
106                                String parent = attributes.getValue(ATTR_PARENT);
107                                mCurrentStyle = new StyleResourceValue(type, name, parent,
108                                        mIsFramework);
109                                mRepository.addResourceValue(mCurrentStyle);
110                                break;
111                            case DECLARE_STYLEABLE:
112                                mCurrentDeclareStyleable = new DeclareStyleableResourceValue(
113                                        type, name, mIsFramework);
114                                mRepository.addResourceValue(mCurrentDeclareStyleable);
115                                break;
116                            case ATTR:
117                                mCurrentAttr = new AttrResourceValue(type, name, mIsFramework);
118                                mRepository.addResourceValue(mCurrentAttr);
119                                break;
120                            default:
121                                mCurrentValue = new ResourceValue(type, name, mIsFramework);
122                                mRepository.addResourceValue(mCurrentValue);
123                                break;
124                        }
125                    }
126                }
127            } else if (mDepth == 3) {
128                // get the resource name
129                String name = attributes.getValue(ATTR_NAME);
130                if (name != null) {
131
132                    if (mCurrentStyle != null) {
133                        // is the attribute in the android namespace?
134                        boolean isFrameworkAttr = mIsFramework;
135                        if (name.startsWith(DEFAULT_NS_PREFIX)) {
136                            name = name.substring(DEFAULT_NS_PREFIX_LEN);
137                            isFrameworkAttr = true;
138                        }
139
140                        mCurrentValue = new ResourceValue(null, name, mIsFramework);
141                        mCurrentStyle.addValue(mCurrentValue, isFrameworkAttr);
142                    } else if (mCurrentDeclareStyleable != null) {
143                        // is the attribute in the android namespace?
144                        boolean isFramework = mIsFramework;
145                        if (name.startsWith(DEFAULT_NS_PREFIX)) {
146                            name = name.substring(DEFAULT_NS_PREFIX_LEN);
147                            isFramework = true;
148                        }
149
150                        mCurrentAttr = new AttrResourceValue(ResourceType.ATTR, name, isFramework);
151                        mCurrentDeclareStyleable.addValue(mCurrentAttr);
152
153                        // also add it to the repository.
154                        mRepository.addResourceValue(mCurrentAttr);
155
156                    } else if (mCurrentAttr != null) {
157                        // get the enum/flag value
158                        String value = attributes.getValue(ATTR_VALUE);
159
160                        try {
161                            // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we
162                            // use Long.decode instead.
163                            mCurrentAttr.addValue(name, (int)(long)Long.decode(value));
164                        } catch (NumberFormatException e) {
165                            // pass, we'll just ignore this value
166                        }
167
168                    }
169                }
170            } else if (mDepth == 4 && mCurrentAttr != null) {
171                // get the enum/flag name
172                String name = attributes.getValue(ATTR_NAME);
173                String value = attributes.getValue(ATTR_VALUE);
174
175                try {
176                    // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we
177                    // use Long.decode instead.
178                    mCurrentAttr.addValue(name, (int)(long)Long.decode(value));
179                } catch (NumberFormatException e) {
180                    // pass, we'll just ignore this value
181                }
182            }
183        } finally {
184            super.startElement(uri, localName, qName, attributes);
185        }
186    }
187
188    private ResourceType getType(String qName, Attributes attributes) {
189        String typeValue;
190
191        // if the node is <item>, we get the type from the attribute "type"
192        if (NODE_ITEM.equals(qName)) {
193            typeValue = attributes.getValue(ATTR_TYPE);
194        } else {
195            // the type is the name of the node.
196            typeValue = qName;
197        }
198
199        ResourceType type = ResourceType.getEnum(typeValue);
200        return type;
201    }
202
203
204    @Override
205    public void characters(char[] ch, int start, int length) throws SAXException {
206        if (mCurrentValue != null) {
207            String value = mCurrentValue.getValue();
208            if (value == null) {
209                mCurrentValue.setValue(new String(ch, start, length));
210            } else {
211                mCurrentValue.setValue(value + new String(ch, start, length));
212            }
213        }
214    }
215
216    public static String trimXmlWhitespaces(String value) {
217        if (value == null) {
218            return null;
219        }
220
221        // look for carriage return and replace all whitespace around it by just 1 space.
222        int index;
223
224        while ((index = value.indexOf('\n')) != -1) {
225            // look for whitespace on each side
226            int left = index - 1;
227            while (left >= 0) {
228                if (Character.isWhitespace(value.charAt(left))) {
229                    left--;
230                } else {
231                    break;
232                }
233            }
234
235            int right = index + 1;
236            int count = value.length();
237            while (right < count) {
238                if (Character.isWhitespace(value.charAt(right))) {
239                    right++;
240                } else {
241                    break;
242                }
243            }
244
245            // remove all between left and right (non inclusive) and replace by a single space.
246            String leftString = null;
247            if (left >= 0) {
248                leftString = value.substring(0, left + 1);
249            }
250            String rightString = null;
251            if (right < count) {
252                rightString = value.substring(right);
253            }
254
255            if (leftString != null) {
256                value = leftString;
257                if (rightString != null) {
258                    value += " " + rightString;
259                }
260            } else {
261                value = rightString != null ? rightString : "";
262            }
263        }
264
265        // now we un-escape the string
266        int length = value.length();
267        char[] buffer = value.toCharArray();
268
269        for (int i = 0 ; i < length ; i++) {
270            if (buffer[i] == '\\' && i + 1 < length) {
271                if (buffer[i+1] == 'u') {
272                    if (i + 5 < length) {
273                        // this is unicode char \u1234
274                        int unicodeChar = Integer.parseInt(new String(buffer, i+2, 4), 16);
275
276                        // put the unicode char at the location of the \
277                        buffer[i] = (char)unicodeChar;
278
279                        // offset the rest of the buffer since we go from 6 to 1 char
280                        if (i + 6 < buffer.length) {
281                            System.arraycopy(buffer, i+6, buffer, i+1, length - i - 6);
282                        }
283                        length -= 5;
284                    }
285                } else {
286                    if (buffer[i+1] == 'n') {
287                        // replace the 'n' char with \n
288                        buffer[i+1] = '\n';
289                    }
290
291                    // offset the buffer to erase the \
292                    System.arraycopy(buffer, i+1, buffer, i, length - i - 1);
293                    length--;
294                }
295            } else if (buffer[i] == '"') {
296                // if the " was escaped it would have been processed above.
297                // offset the buffer to erase the "
298                System.arraycopy(buffer, i+1, buffer, i, length - i - 1);
299                length--;
300
301                // unlike when unescaping, we want to process the next char too
302                i--;
303            }
304        }
305
306        return new String(buffer, 0, length);
307    }
308}
309