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