1 /* 2 * Copyright (C) 2012 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 17 package com.motorola.studio.android.model.manifest; 18 19 import java.io.IOException; 20 import java.io.StringWriter; 21 import java.util.LinkedList; 22 import java.util.List; 23 import java.util.Map; 24 25 import javax.xml.parsers.DocumentBuilder; 26 import javax.xml.parsers.DocumentBuilderFactory; 27 import javax.xml.parsers.ParserConfigurationException; 28 29 import org.apache.xml.serialize.OutputFormat; 30 import org.apache.xml.serialize.XMLSerializer; 31 import org.eclipse.core.runtime.IStatus; 32 import org.eclipse.jface.text.IDocument; 33 import org.w3c.dom.Attr; 34 import org.w3c.dom.Document; 35 import org.w3c.dom.NamedNodeMap; 36 import org.w3c.dom.Node; 37 38 import com.motorola.studio.android.common.exception.AndroidException; 39 import com.motorola.studio.android.common.log.StudioLogger; 40 import com.motorola.studio.android.common.utilities.AndroidStatus; 41 import com.motorola.studio.android.common.utilities.i18n.UtilitiesNLS; 42 import com.motorola.studio.android.model.manifest.dom.AbstractBuildingBlockNode; 43 import com.motorola.studio.android.model.manifest.dom.ActionNode; 44 import com.motorola.studio.android.model.manifest.dom.ActivityNode; 45 import com.motorola.studio.android.model.manifest.dom.AndroidManifestNode; 46 import com.motorola.studio.android.model.manifest.dom.AndroidManifestNode.NodeType; 47 import com.motorola.studio.android.model.manifest.dom.ApplicationNode; 48 import com.motorola.studio.android.model.manifest.dom.CommentNode; 49 import com.motorola.studio.android.model.manifest.dom.IntentFilterNode; 50 import com.motorola.studio.android.model.manifest.dom.ManifestNode; 51 import com.motorola.studio.android.model.manifest.parser.AndroidManifestParser; 52 53 /** 54 * Class that represents an AndroidManifest.xml file 55 */ 56 @SuppressWarnings("deprecation") 57 public class AndroidManifestFile extends AndroidManifestParser 58 { 59 /** 60 * Adds an AndroidManifestNode to the file 61 * 62 * @param node The node to be added 63 */ 64 public void addNode(AndroidManifestNode node) 65 { 66 if ((node != null) && !rootNodes.contains(node)) 67 { 68 rootNodes.add(node); 69 } 70 } 71 72 /** 73 * Removes an AndroidManifestNode from the file 74 * 75 * @param node The node to be removed 76 */ 77 public void removeNode(AndroidManifestNode node) 78 { 79 if ((node != null) && !rootNodes.contains(node)) 80 { 81 rootNodes.add(node); 82 } 83 } 84 85 /** 86 * Retrieves an array containing all nodes present on xml root. 87 * If the file is valid, there will be only one node, the <manifest> node 88 * 89 * @return an array containing all nodes present on xml root. 90 */ 91 public AndroidManifestNode[] getNodes() 92 { 93 AndroidManifestNode[] nodes = new AndroidManifestNode[rootNodes.size()]; 94 nodes = rootNodes.toArray(nodes); 95 96 return nodes; 97 } 98 99 /** 100 * Retrieves the <manifest> node 101 * 102 * @return the <manifest> node 103 */ 104 public ManifestNode getManifestNode() 105 { 106 ManifestNode manifestNode = null; 107 108 for (AndroidManifestNode node : rootNodes) 109 { 110 if (node.getNodeType() == NodeType.Manifest) 111 { 112 manifestNode = (ManifestNode) node; 113 break; 114 } 115 } 116 117 return manifestNode; 118 } 119 120 /** 121 * Retrieves the <application> node from the manifest file 122 * 123 * @return The <application> node of the manifest file. 124 */ 125 public ApplicationNode getApplicationNode() 126 { 127 // Retrieve <manifest> node and return application node 128 return getManifestNode().getApplicationNode(); 129 130 } 131 132 /** 133 * Retrieves a building block node, which can be of the following types: 134 * NodeType.Activity 135 * NodeType.Provider 136 * NodeType.Receiver 137 * NodeType.Service 138 * 139 * @param type The NodeType. 140 * @param androidName The android:name property value. Should be the fully qualified name of the building block class. 141 * For example, for the Activity class "Test" located in the package "com.motorola", the androidName parameter should be "com.motorola.Teste" 142 * 143 * @return A AbstractBuildingBlockNode that represents the building block. If no matching node is found or 144 * the type passed is invalid, null is returned. 145 */ 146 public AbstractBuildingBlockNode getBuildingBlockNode(NodeType type, String androidName) 147 { 148 // Result 149 AbstractBuildingBlockNode resultNode = null; 150 151 // Candidate list of nodes to iterate through 152 List<AbstractBuildingBlockNode> candidateList = new LinkedList<AbstractBuildingBlockNode>(); 153 154 // Retrieve the Manifest node to check the default package 155 ManifestNode manifestNode = getManifestNode(); 156 String manifestPackage = manifestNode.getNodeProperties().get(PROP_PACKAGE); 157 158 // Compare the qualified name from the parameter with the manifest package. If equal, we can use the class name for comparison purposes. 159 String androidNamePackage = androidName.substring(0, androidName.lastIndexOf('.')); 160 String shortAndroidName = new String(); 161 162 if (manifestPackage.equals(androidNamePackage)) 163 { 164 shortAndroidName = androidName.substring(androidName.lastIndexOf('.')); 165 } 166 167 // Retrieve the application node 168 ApplicationNode applicationNode = getApplicationNode(); 169 170 // Check the building block type 171 switch (type) 172 { 173 case Activity: 174 candidateList.addAll(applicationNode.getActivityNodes()); 175 break; 176 case Provider: 177 candidateList.addAll(applicationNode.getProviderNodes()); 178 break; 179 case Receiver: 180 candidateList.addAll(applicationNode.getReceiverNodes()); 181 break; 182 case Service: 183 candidateList.addAll(applicationNode.getServiceNodes()); 184 break; 185 default: 186 break; 187 } 188 189 // Search the candidate list for the target node 190 for (AbstractBuildingBlockNode node : candidateList) 191 { 192 // In the case that shortAndroidName is not null or empty, we check if it's like that in the manifest first 193 if ((shortAndroidName != null) && (shortAndroidName.length() > 0)) 194 { 195 if (node.getNodeProperties().get(PROP_NAME).equals(shortAndroidName)) 196 { 197 resultNode = node; 198 break; 199 } 200 } 201 202 if (node.getNodeProperties().get(PROP_NAME).equals(androidName)) 203 { 204 // We found the node! 205 resultNode = node; 206 break; 207 } 208 } 209 210 return resultNode; 211 } 212 213 /** 214 * Retrieves an IDocument object containing the xml content for the file 215 * 216 * @return an IDocument object containing the xml content for the file 217 */ 218 public IDocument getContent() throws AndroidException 219 { 220 IDocument document = null; 221 DocumentBuilder documentBuilder = null; 222 223 try 224 { 225 documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 226 } 227 catch (ParserConfigurationException e) 228 { 229 StudioLogger.error(AndroidManifestFile.class, 230 UtilitiesNLS.EXC_AndroidManifestFile_ErrorCreatingTheDocumentBuilder, e); 231 throw new AndroidException( 232 UtilitiesNLS.EXC_AndroidManifestFile_ErrorCreatingTheDocumentBuilder); 233 } 234 235 Document xmlDocument = documentBuilder.newDocument(); 236 237 for (AndroidManifestNode node : rootNodes) 238 { 239 addNode(xmlDocument, null, node); 240 } 241 242 document = new org.eclipse.jface.text.Document(getXmlContent(xmlDocument)); 243 244 return document; 245 } 246 247 /** 248 * Recursive function to build a XML file from AndroidManifestNode objects 249 * 250 * @param xmlDocument The XML Document 251 * @param xmlParentNode The XML parent node 252 * @param nodeToAdd The AndroidManifestNode to be added 253 */ 254 private void addNode(Document xmlDocument, Node xmlParentNode, AndroidManifestNode nodeToAdd) 255 { 256 Node xmlNode; 257 258 if (nodeToAdd instanceof CommentNode) 259 { 260 CommentNode commentNode = (CommentNode) nodeToAdd; 261 xmlNode = xmlDocument.createComment(commentNode.getComment()); 262 } 263 else 264 { 265 xmlNode = xmlDocument.createElement(nodeToAdd.getNodeName()); 266 Map<String, String> attributes = nodeToAdd.getNodeProperties(); 267 Map<String, String> unknownAttributes = nodeToAdd.getNodeUnknownProperties(); 268 AndroidManifestNode[] children = nodeToAdd.getChildren(); 269 AndroidManifestNode[] unknown = nodeToAdd.getUnkownChildren(); 270 271 // Adds valid attributes 272 if ((attributes != null) && (attributes.size() > 0)) 273 { 274 NamedNodeMap xmlAttributes = xmlNode.getAttributes(); 275 276 for (String attrName : attributes.keySet()) 277 { 278 Attr attr = xmlDocument.createAttribute(attrName); 279 attr.setValue(attributes.get(attrName)); 280 xmlAttributes.setNamedItem(attr); 281 } 282 } 283 284 // Adds invalid attributes 285 if ((unknownAttributes != null) && (unknownAttributes.size() > 0)) 286 { 287 NamedNodeMap xmlAttributes = xmlNode.getAttributes(); 288 289 for (String attrName : unknownAttributes.keySet()) 290 { 291 Attr attr = xmlDocument.createAttribute(attrName); 292 attr.setNodeValue(unknownAttributes.get(attrName)); 293 xmlAttributes.setNamedItem(attr); 294 } 295 } 296 297 // Adds known child nodes 298 for (AndroidManifestNode child : children) 299 { 300 addNode(xmlDocument, xmlNode, child); 301 } 302 303 // Adds unknown child nodes 304 for (AndroidManifestNode child : unknown) 305 { 306 addNode(xmlDocument, xmlNode, child); 307 } 308 } 309 310 if (xmlParentNode == null) 311 { 312 xmlDocument.appendChild(xmlNode); 313 } 314 else 315 { 316 xmlParentNode.appendChild(xmlNode); 317 } 318 } 319 320 /** 321 * Creates the XML content from a XML Document 322 * 323 * @param xmlDocument The XML Document 324 * @return a String object containing the XML content 325 */ 326 private String getXmlContent(Document xmlDocument) throws AndroidException 327 { 328 // Despite Xerces is deprecated, its formatted xml source output works 329 // better than W3C xml output classes 330 OutputFormat outputFormat = new OutputFormat(); 331 XMLSerializer xmlSerializer = new XMLSerializer(); 332 StringWriter writer = new StringWriter(); 333 String content = null; 334 335 outputFormat.setEncoding("UTF-8"); 336 outputFormat.setLineSeparator(System.getProperty("line.separator")); 337 outputFormat.setIndenting(true); 338 339 xmlSerializer.setOutputCharStream(writer); 340 xmlSerializer.setOutputFormat(outputFormat); 341 342 try 343 { 344 xmlSerializer.serialize(xmlDocument); 345 content = writer.toString(); 346 } 347 catch (IOException e) 348 { 349 StudioLogger.error(AndroidManifestFile.class, 350 UtilitiesNLS.EXC_AndroidManifestFile_ErrorFormattingTheXMLOutput, e); 351 throw new AndroidException( 352 UtilitiesNLS.EXC_AndroidManifestFile_ErrorFormattingTheXMLOutput); 353 } 354 finally 355 { 356 if (writer != null) 357 { 358 try 359 { 360 writer.close(); 361 } 362 catch (IOException e) 363 { 364 //Do nothing. 365 } 366 } 367 } 368 369 return content; 370 } 371 372 /** 373 * Gets all file problems: Errors and Warnings 374 * 375 * @return all file problems 376 */ 377 public IStatus[] getProblems() 378 { 379 ManifestNode manifestNode = getManifestNode(); 380 IStatus[] errors; 381 382 if (manifestNode == null) 383 { 384 errors = 385 new IStatus[] 386 { 387 new AndroidStatus( 388 IStatus.ERROR, 389 UtilitiesNLS.ERR_AndroidManifestFile_TheFileAndroidManifestXmlIsMalFormed) 390 }; 391 } 392 else 393 { 394 errors = getManifestNode().getRecursiveNodeErrors(); 395 } 396 397 return errors; 398 } 399 400 /** 401 * Gets all file errors 402 * 403 * @return all file errors 404 */ 405 public IStatus[] getErrors() 406 { 407 List<IStatus> errors = new LinkedList<IStatus>(); 408 409 for (IStatus status : getProblems()) 410 { 411 if (status.getSeverity() == IStatus.ERROR) 412 { 413 errors.add(status); 414 } 415 } 416 417 return errors.toArray(new IStatus[0]); 418 } 419 420 /** 421 * Checks if the file has errors in the model 422 * 423 * @return true if the file has errors in the model and false otherwise 424 */ 425 public boolean hasErrors() 426 { 427 boolean hasErrors = false; 428 429 for (IStatus status : getProblems()) 430 { 431 if (status.getSeverity() == IStatus.ERROR) 432 { 433 hasErrors = true; 434 break; 435 } 436 } 437 438 return hasErrors; 439 } 440 441 public AndroidManifestNode getNode(NodeType nodeType) 442 { 443 AndroidManifestNode requiredNode = null; 444 AndroidManifestNode[] manifestChildren = null; 445 for (AndroidManifestNode node : rootNodes) 446 { 447 if (node instanceof ManifestNode) 448 { 449 manifestChildren = ((ManifestNode) node).getChildren(); 450 break; 451 } 452 } 453 454 if ((manifestChildren != null) && (manifestChildren.length > 0)) 455 { 456 for (AndroidManifestNode manifestChild : manifestChildren) 457 { 458 if (manifestChild.getNodeType().equals(nodeType)) 459 { 460 requiredNode = manifestChild; 461 break; 462 } 463 } 464 } 465 return requiredNode; 466 } 467 468 /** 469 * This method sets the main activity of and android project be the class identified by {@code className}. 470 * 471 * @param className The name of the class to be set as the main activity. 472 * @param isMainActivity If true, the activity will be set as main activity. If false, the activity will no longer be a main activity. 473 * @return True if the activity exist, is declared on the manifest and was successfully set as the main activity. False otherwise. 474 * */ 475 public boolean setAsMainActivity(String className, boolean isMainActivity) 476 { 477 boolean result = false; 478 479 List<ActivityNode> activityNodes = getApplicationNode().getActivityNodes(); 480 481 for (ActivityNode activityNode : activityNodes) 482 { 483 if (activityNode.getName().equals(className)) 484 { 485 result = activityNode.setAsMainActivity(isMainActivity); 486 break; 487 } 488 } 489 490 return result; 491 } 492 493 /** 494 * Convenience method that returns the main activity of the application. 495 * The main activity is the one declared with the intent filter 496 * <action android:name="android.intent.action.MAIN"/> 497 * If more than one main activity is declared, then the first one declared is returned. 498 * This behavior follows the android behavior to choose the main activity. 499 * */ 500 public ActivityNode getMainActivity() 501 { 502 ActivityNode mainActivity = null; 503 ApplicationNode appNode = getApplicationNode(); 504 List<ActivityNode> activities = appNode.getActivityNodes(); 505 506 for (ActivityNode activity : activities) 507 { 508 for (IntentFilterNode intent : activity.getIntentFilterNodes()) 509 { 510 for (ActionNode actionNode : intent.getActionNodes()) 511 { 512 if (actionNode.getNodeProperties().get("android:name") 513 .equals("android.intent.action.MAIN")) 514 { 515 mainActivity = activity; 516 break; 517 } 518 } 519 if (mainActivity != null) 520 { 521 break; 522 } 523 } 524 if (mainActivity != null) 525 { 526 break; 527 } 528 } 529 530 return mainActivity; 531 } 532 533 } 534