private void performPlaceHolderSubstitution( ManifestInfo manifestInfo, XmlDocument xmlDocument, MergingReport.Builder mergingReportBuilder) { // check for placeholders presence, switch first the packageName and application id if // it is not explicitly set. Map<String, Object> finalPlaceHolderValues = mPlaceHolderValues; if (!mPlaceHolderValues.containsKey("applicationId")) { String packageName = manifestInfo.getMainManifestPackageName().isPresent() ? manifestInfo.getMainManifestPackageName().get() : xmlDocument.getPackageName(); // add all existing placeholders except package name that will be swapped. ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder(); for (Map.Entry<String, Object> entry : mPlaceHolderValues.entrySet()) { if (!entry.getKey().equals(PlaceholderHandler.PACKAGE_NAME)) { builder.put(entry); } } finalPlaceHolderValues = builder .put(PlaceholderHandler.PACKAGE_NAME, packageName) .put(PlaceholderHandler.APPLICATION_ID, packageName) .build(); } KeyBasedValueResolver<String> placeHolderValueResolver = new MapBasedKeyBasedValueResolver<String>(finalPlaceHolderValues); PlaceholderHandler placeholderHandler = new PlaceholderHandler(); placeholderHandler.visit( mMergeType, xmlDocument, placeHolderValueResolver, mergingReportBuilder); }
public void testPlaceholders() throws ParserConfigurationException, SAXException, IOException { String xml = "" + "<manifest\n" + " xmlns:android=\"http://schemas.android.com/apk/res/android\">\n" + " <activity android:name=\"activityOne\"\n" + " android:attr1=\"${landscapePH}\"\n" + " android:attr2=\"prefix.${landscapePH}\"\n" + " android:attr3=\"${landscapePH}.suffix\"\n" + " android:attr4=\"prefix${landscapePH}suffix\">\n" + " </activity>\n" + "</manifest>"; XmlDocument refDocument = TestUtils.xmlDocumentFromString( TestUtils.sourceFile(getClass(), "testPlaceholders#xml"), xml); PlaceholderHandler handler = new PlaceholderHandler(); handler.visit( ManifestMerger2.MergeType.APPLICATION, refDocument, new KeyBasedValueResolver<String>() { @Override public String getValue(@NonNull String key) { return "newValue"; } }, mBuilder); Optional<XmlElement> activityOne = refDocument .getRootNode() .getNodeByTypeAndKey(ManifestModel.NodeTypes.ACTIVITY, ".activityOne"); assertTrue(activityOne.isPresent()); assertEquals(5, activityOne.get().getAttributes().size()); // check substitution. assertEquals( "newValue", activityOne.get().getAttribute(XmlNode.fromXmlName("android:attr1")).get().getValue()); assertEquals( "prefix.newValue", activityOne.get().getAttribute(XmlNode.fromXmlName("android:attr2")).get().getValue()); assertEquals( "newValue.suffix", activityOne.get().getAttribute(XmlNode.fromXmlName("android:attr3")).get().getValue()); assertEquals( "prefixnewValuesuffix", activityOne.get().getAttribute(XmlNode.fromXmlName("android:attr4")).get().getValue()); for (XmlAttribute xmlAttribute : activityOne.get().getAttributes()) { // any attribute other than android:name should have been injected. if (!xmlAttribute.getName().toString().contains("name")) { verify(mActionRecorder) .recordAttributeAction( xmlAttribute, SourcePosition.UNKNOWN, Actions.ActionType.INJECTED, null); } } }
public void testPlaceHolder_notProvided_inLibrary() throws ParserConfigurationException, SAXException, IOException { String xml = "" + "<manifest\n" + " xmlns:android=\"http://schemas.android.com/apk/res/android\">\n" + " <activity android:name=\"activityOne\"\n" + " android:attr1=\"${landscapePH}\"/>\n" + "</manifest>"; XmlDocument refDocument = TestUtils.xmlDocumentFromString( TestUtils.sourceFile(getClass(), "testPlaceholders#xml"), xml); PlaceholderHandler handler = new PlaceholderHandler(); handler.visit(ManifestMerger2.MergeType.LIBRARY, refDocument, nullResolver, mBuilder); // verify the error was recorded. verify(mBuilder) .addMessage( any(SourceFilePosition.class), eq(MergingReport.Record.Severity.INFO), anyString()); }
/** * Perform high level ordering of files merging and delegates actual merging to {@link * XmlDocument#merge(XmlDocument, com.android.manifmerger.MergingReport.Builder)} * * @return the merging activity report. * @throws MergeFailureException if the merging cannot be completed (for instance, if xml files * cannot be loaded). */ private MergingReport merge() throws MergeFailureException { // initiate a new merging report MergingReport.Builder mergingReportBuilder = new MergingReport.Builder(mLogger); SelectorResolver selectors = new SelectorResolver(); // load all the libraries xml files up front to have a list of all possible node:selector // values. List<LoadedManifestInfo> loadedLibraryDocuments = loadLibraries(selectors, mergingReportBuilder); // load the main manifest file to do some checking along the way. LoadedManifestInfo loadedMainManifestInfo = load( new ManifestInfo( mManifestFile.getName(), mManifestFile, XmlDocument.Type.MAIN, Optional.<String>absent() /* mainManifestPackageName */), selectors, mergingReportBuilder); // first do we have a package declaration in the main manifest ? Optional<XmlAttribute> mainPackageAttribute = loadedMainManifestInfo.getXmlDocument().getPackage(); if (!mainPackageAttribute.isPresent()) { mergingReportBuilder.addMessage( loadedMainManifestInfo.getXmlDocument().getSourceLocation(), 0, 0, MergingReport.Record.Severity.ERROR, String.format( "Main AndroidManifest.xml at %1$s manifest:package attribute " + "is not declared", loadedMainManifestInfo.getXmlDocument().getSourceLocation().print(true))); return mergingReportBuilder.build(); } // check for placeholders presence. Map<String, Object> finalPlaceHolderValues = mPlaceHolderValues; if (!mPlaceHolderValues.containsKey(APPLICATION_ID)) { finalPlaceHolderValues = ImmutableMap.<String, Object>builder() .putAll(mPlaceHolderValues) .put(PACKAGE_NAME, mainPackageAttribute.get().getValue()) .put(APPLICATION_ID, mainPackageAttribute.get().getValue()) .build(); } // perform system property injection performSystemPropertiesInjection(mergingReportBuilder, loadedMainManifestInfo.getXmlDocument()); // force the re-parsing of the xml as elements may have been added through system // property injection. loadedMainManifestInfo = new LoadedManifestInfo( loadedMainManifestInfo, loadedMainManifestInfo.getOriginalPackageName(), loadedMainManifestInfo.getXmlDocument().reparse()); // invariant : xmlDocumentOptional holds the higher priority document and we try to // merge in lower priority documents. Optional<XmlDocument> xmlDocumentOptional = Optional.absent(); for (File inputFile : mFlavorsAndBuildTypeFiles) { mLogger.info("Merging flavors and build manifest %s \n", inputFile.getPath()); LoadedManifestInfo overlayDocument = load( new ManifestInfo( null, inputFile, XmlDocument.Type.OVERLAY, Optional.of(mainPackageAttribute.get().getValue())), selectors, mergingReportBuilder); // check package declaration. Optional<XmlAttribute> packageAttribute = overlayDocument.getXmlDocument().getPackage(); // if both files declare a package name, it should be the same. if (loadedMainManifestInfo.getOriginalPackageName().isPresent() && packageAttribute.isPresent() && !loadedMainManifestInfo .getOriginalPackageName() .get() .equals(packageAttribute.get().getValue())) { // no suggestion for library since this is actually forbidden to change the // the package name per flavor. String message = mMergeType == MergeType.APPLICATION ? String.format( "Overlay manifest:package attribute declared at %1$s value=(%2$s)\n" + "\thas a different value=(%3$s) " + "declared in main manifest at %4$s\n" + "\tSuggestion: remove the overlay declaration at %5$s " + "\tand place it in the build.gradle:\n" + "\t\tflavorName {\n" + "\t\t\tapplicationId = \"%2$s\"\n" + "\t\t}", packageAttribute.get().printPosition(), packageAttribute.get().getValue(), mainPackageAttribute.get().getValue(), mainPackageAttribute.get().printPosition(), packageAttribute.get().getSourceLocation().print(true)) : String.format( "Overlay manifest:package attribute declared at %1$s value=(%2$s)\n" + "\thas a different value=(%3$s) " + "declared in main manifest at %4$s", packageAttribute.get().printPosition(), packageAttribute.get().getValue(), mainPackageAttribute.get().getValue(), mainPackageAttribute.get().printPosition()); mergingReportBuilder.addMessage( overlayDocument.getXmlDocument().getSourceLocation(), 0, 0, MergingReport.Record.Severity.ERROR, message); return mergingReportBuilder.build(); } overlayDocument .getXmlDocument() .getRootNode() .getXml() .setAttribute("package", mainPackageAttribute.get().getValue()); xmlDocumentOptional = merge(xmlDocumentOptional, overlayDocument, mergingReportBuilder); if (!xmlDocumentOptional.isPresent()) { return mergingReportBuilder.build(); } } mLogger.info("Merging main manifest %s\n", mManifestFile.getPath()); xmlDocumentOptional = merge(xmlDocumentOptional, loadedMainManifestInfo, mergingReportBuilder); if (!xmlDocumentOptional.isPresent()) { return mergingReportBuilder.build(); } // force main manifest package into resulting merged file when creating a library manifest. if (mMergeType == MergeType.LIBRARY) { // extract the package name... String mainManifestPackageName = loadedMainManifestInfo.getXmlDocument().getRootNode().getXml().getAttribute("package"); // save it in the selector instance. if (!Strings.isNullOrEmpty(mainManifestPackageName)) { xmlDocumentOptional .get() .getRootNode() .getXml() .setAttribute("package", mainManifestPackageName); } } for (LoadedManifestInfo libraryDocument : loadedLibraryDocuments) { mLogger.info("Merging library manifest " + libraryDocument.getLocation()); xmlDocumentOptional = merge(xmlDocumentOptional, libraryDocument, mergingReportBuilder); if (!xmlDocumentOptional.isPresent()) { return mergingReportBuilder.build(); } } // done with proper merging phase, now we need to trim unwanted elements, placeholder // substitution and system properties injection. ElementsTrimmer.trim(xmlDocumentOptional.get(), mergingReportBuilder); if (mergingReportBuilder.hasErrors()) { return mergingReportBuilder.build(); } // do one last placeholder substitution, this is useful as we don't stop the build // when a library failed a placeholder substitution, but the element might have // been overridden so the problem was transient. However, with the final document // ready, all placeholders values must have been provided. KeyBasedValueResolver<String> placeHolderValueResolver = new MapBasedKeyBasedValueResolver<String>(finalPlaceHolderValues); PlaceholderHandler placeholderHandler = new PlaceholderHandler(); placeholderHandler.visit( mMergeType, xmlDocumentOptional.get(), placeHolderValueResolver, mergingReportBuilder); if (mergingReportBuilder.hasErrors()) { return mergingReportBuilder.build(); } // perform system property injection. performSystemPropertiesInjection(mergingReportBuilder, xmlDocumentOptional.get()); XmlDocument finalMergedDocument = xmlDocumentOptional.get(); PostValidator.validate(finalMergedDocument, mergingReportBuilder); if (mergingReportBuilder.hasErrors()) { finalMergedDocument .getRootNode() .addMessage( mergingReportBuilder, MergingReport.Record.Severity.WARNING, "Post merge validation failed"); } // only remove tools annotations if we are packaging an application. if (mOptionalFeatures.contains(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)) { finalMergedDocument = ToolsInstructionsCleaner.cleanToolsReferences(finalMergedDocument, mLogger); } if (mOptionalFeatures.contains(Invoker.Feature.EXTRACT_FQCNS)) { extractFcqns(finalMergedDocument); } if (finalMergedDocument != null) { mergingReportBuilder.setMergedDocument(finalMergedDocument); } MergingReport mergingReport = mergingReportBuilder.build(); StdLogger stdLogger = new StdLogger(StdLogger.Level.INFO); mergingReport.log(stdLogger); stdLogger.verbose(mergingReport.getMergedDocument().get().prettyPrint()); if (mReportFile.isPresent()) { writeReport(mergingReport); } return mergingReport; }