1 /* 2 * Copyright (C) 2010 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.android.sdklib.internal.project; 18 19 import com.android.SdkConstants; 20 import com.android.annotations.NonNull; 21 import com.android.io.IAbstractFile; 22 import com.android.io.IAbstractFolder; 23 import com.android.io.StreamException; 24 25 import java.io.BufferedReader; 26 import java.io.ByteArrayOutputStream; 27 import java.io.IOException; 28 import java.io.InputStreamReader; 29 import java.io.OutputStream; 30 import java.io.OutputStreamWriter; 31 import java.util.HashMap; 32 import java.util.HashSet; 33 import java.util.Map; 34 import java.util.Map.Entry; 35 import java.util.regex.Matcher; 36 37 /** 38 * A modifyable and saveable copy of a {@link ProjectProperties}. 39 * <p/>This copy gives access to modification method such as {@link #setProperty(String, String)} 40 * and {@link #removeProperty(String)}. 41 * 42 * To get access to an instance, use {@link ProjectProperties#makeWorkingCopy()} or 43 * {@link ProjectProperties#create(IAbstractFolder, PropertyType)}. 44 */ 45 public class ProjectPropertiesWorkingCopy extends ProjectProperties { 46 47 private final static Map<String, String> COMMENT_MAP = new HashMap<String, String>(); 48 static { 49 // 1-------10--------20--------30--------40--------50--------60--------70--------80 COMMENT_MAP.put(PROPERTY_TARGET, "# Project target.\\n")50 COMMENT_MAP.put(PROPERTY_TARGET, 51 "# Project target.\n"); COMMENT_MAP.put(PROPERTY_SPLIT_BY_DENSITY, "# Indicates whether an apk should be generated for each density.\\n")52 COMMENT_MAP.put(PROPERTY_SPLIT_BY_DENSITY, 53 "# Indicates whether an apk should be generated for each density.\n"); COMMENT_MAP.put(PROPERTY_SDK, "# location of the SDK. This is only used by Ant\\n" + "# For customization when using a Version Control System, please read the\\n" + "# header note.\\n")54 COMMENT_MAP.put(PROPERTY_SDK, 55 "# location of the SDK. This is only used by Ant\n" + 56 "# For customization when using a Version Control System, please read the\n" + 57 "# header note.\n"); COMMENT_MAP.put(PROPERTY_PACKAGE, "# Package of the application being exported\\n")58 COMMENT_MAP.put(PROPERTY_PACKAGE, 59 "# Package of the application being exported\n"); COMMENT_MAP.put(PROPERTY_VERSIONCODE, "# Major version code\\n")60 COMMENT_MAP.put(PROPERTY_VERSIONCODE, 61 "# Major version code\n"); COMMENT_MAP.put(PROPERTY_PROJECTS, "# List of the Android projects being used for the export.\\n" + "# The list is made of paths that are relative to this project,\\n" + "# using forward-slash (/) as separator, and are separated by colons (:).\\n")62 COMMENT_MAP.put(PROPERTY_PROJECTS, 63 "# List of the Android projects being used for the export.\n" + 64 "# The list is made of paths that are relative to this project,\n" + 65 "# using forward-slash (/) as separator, and are separated by colons (:).\n"); 66 } 67 68 69 /** 70 * Sets a new properties. If a property with the same name already exists, it is replaced. 71 * @param name the name of the property. 72 * @param value the value of the property. 73 */ setProperty(String name, String value)74 public synchronized void setProperty(String name, String value) { 75 mProperties.put(name, value); 76 } 77 78 /** 79 * Removes a property and returns its previous value (or null if the property did not exist). 80 * @param name the name of the property to remove. 81 */ removeProperty(String name)82 public synchronized String removeProperty(String name) { 83 return mProperties.remove(name); 84 } 85 86 /** 87 * Merges all properties from the given file into the current properties. 88 * <p/> 89 * This emulates the Ant behavior: existing properties are <em>not</em> overridden. 90 * Only new undefined properties become defined. 91 * <p/> 92 * Typical usage: 93 * <ul> 94 * <li>Create a ProjectProperties with {@code PropertyType#ANT} 95 * <li>Merge in values using {@code PropertyType#PROJECT} 96 * <li>The result is that this contains all the properties from default plus those 97 * overridden by the build.properties file. 98 * </ul> 99 * 100 * @param type One the possible {@link ProjectProperties.PropertyType}s. 101 * @return this object, for chaining. 102 */ merge(PropertyType type)103 public synchronized ProjectPropertiesWorkingCopy merge(PropertyType type) { 104 if (mProjectFolder.exists() && mType != type) { 105 IAbstractFile propFile = mProjectFolder.getFile(type.getFilename()); 106 if (propFile.exists()) { 107 Map<String, String> map = parsePropertyFile(propFile, null /* log */); 108 if (map != null) { 109 for (Entry<String, String> entry : map.entrySet()) { 110 String key = entry.getKey(); 111 String value = entry.getValue(); 112 if (!mProperties.containsKey(key) && value != null) { 113 mProperties.put(key, value); 114 } 115 } 116 } 117 } 118 } 119 return this; 120 } 121 122 123 /** 124 * Saves the property file, using UTF-8 encoding. 125 * @throws IOException 126 * @throws StreamException 127 */ save()128 public synchronized void save() throws IOException, StreamException { 129 IAbstractFile toSave = mProjectFolder.getFile(mType.getFilename()); 130 131 // write the whole file in a byte array before dumping it in the file. This 132 // This is so that if the file already existing 133 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 134 OutputStreamWriter writer = new OutputStreamWriter(baos, SdkConstants.INI_CHARSET); 135 136 if (toSave.exists()) { 137 BufferedReader reader = new BufferedReader(new InputStreamReader(toSave.getContents(), 138 SdkConstants.INI_CHARSET)); 139 140 // since we're reading the existing file and replacing values with new ones, or skipping 141 // removed values, we need to record what properties have been visited, so that 142 // we can figure later what new properties need to be added at the end of the file. 143 HashSet<String> visitedProps = new HashSet<String>(); 144 145 String line = null; 146 while ((line = reader.readLine()) != null) { 147 // check if this is a line containing a property. 148 if (line.length() > 0 && line.charAt(0) != '#') { 149 150 Matcher m = PATTERN_PROP.matcher(line); 151 if (m.matches()) { 152 String key = m.group(1); 153 String value = m.group(2); 154 155 // record the prop 156 visitedProps.add(key); 157 158 // check if this property must be removed. 159 if (mType.isRemovedProperty(key)) { 160 value = null; 161 } else if (mProperties.containsKey(key)) { // if the property still exists. 162 // put the new value. 163 value = mProperties.get(key); 164 } else { 165 // property doesn't exist. Check if it's a known property. 166 // if it's a known one, we'll remove it, otherwise, leave it untouched. 167 if (mType.isKnownProperty(key)) { 168 value = null; 169 } 170 } 171 172 // if the value is still valid, write it down. 173 if (value != null) { 174 writeValue(writer, key, value, false /*addComment*/); 175 } 176 } else { 177 // the line was wrong, let's just ignore it so that it's removed from the 178 // file. 179 } 180 } else { 181 // non-property line: just write the line in the output as-is. 182 writer.append(line).append('\n'); 183 } 184 } 185 186 // now add the new properties. 187 for (Entry<String, String> entry : mProperties.entrySet()) { 188 if (visitedProps.contains(entry.getKey()) == false) { 189 String value = entry.getValue(); 190 if (value != null) { 191 writeValue(writer, entry.getKey(), value, true /*addComment*/); 192 } 193 } 194 } 195 196 } else { 197 // new file, just write it all 198 199 // write the header (can be null, for example for PropertyType.LEGACY_BUILD) 200 if (mType.getHeader() != null) { 201 writer.write(mType.getHeader()); 202 } 203 204 // write the properties. 205 for (Entry<String, String> entry : mProperties.entrySet()) { 206 String value = entry.getValue(); 207 if (value != null) { 208 writeValue(writer, entry.getKey(), value, true /*addComment*/); 209 } 210 } 211 } 212 213 writer.flush(); 214 215 // now put the content in the file. 216 OutputStream filestream = toSave.getOutputStream(); 217 filestream.write(baos.toByteArray()); 218 filestream.flush(); 219 } 220 writeValue(OutputStreamWriter writer, String key, String value, boolean addComment)221 private void writeValue(OutputStreamWriter writer, String key, String value, 222 boolean addComment) throws IOException { 223 if (addComment) { 224 String comment = COMMENT_MAP.get(key); 225 if (comment != null) { 226 writer.write(comment); 227 } 228 } 229 230 writer.write(String.format("%s=%s\n", key, escape(value))); 231 } 232 233 /** 234 * Private constructor. 235 * <p/> 236 * Use {@link #load(String, PropertyType)} or {@link #create(String, PropertyType)} 237 * to instantiate. 238 */ ProjectPropertiesWorkingCopy(IAbstractFolder projectFolder, Map<String, String> map, PropertyType type)239 ProjectPropertiesWorkingCopy(IAbstractFolder projectFolder, Map<String, String> map, 240 PropertyType type) { 241 super(projectFolder, map, type); 242 } 243 244 @NonNull makeReadOnlyCopy()245 public ProjectProperties makeReadOnlyCopy() { 246 // copy the current properties in a new map 247 Map<String, String> propList = new HashMap<String, String>(mProperties); 248 249 return new ProjectProperties(mProjectFolder, propList, mType); 250 } 251 } 252