/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.sdklib.internal.project; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.io.FolderWrapper; import com.android.io.IAbstractFile; import com.android.io.IAbstractFolder; import com.android.io.StreamException; import com.android.utils.ILogger; import com.google.common.io.Closeables; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Class representing project properties for both ADT and Ant-based build. * <p/>The class is associated to a {@link PropertyType} that indicate which of the project * property file is represented. * <p/>To load an existing file, use {@link #load(IAbstractFolder, PropertyType)}. * <p/>The class is meant to be always in sync (or at least not newer) than the file it represents. * Once created, it can only be updated through {@link #reload()} * * <p/>The make modification or make new file, use a {@link ProjectPropertiesWorkingCopy} instance, * either through {@link #create(IAbstractFolder, PropertyType)} or through * {@link #makeWorkingCopy()}. * */ public class ProjectProperties implements IPropertySource { protected static final Pattern PATTERN_PROP = Pattern.compile( "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); /** The property name for the project target */ public static final String PROPERTY_TARGET = "target"; /** The property name for the renderscript build target */ public static final String PROPERTY_RS_TARGET = "renderscript.target"; /** The property name for the renderscript support mode */ public static final String PROPERTY_RS_SUPPORT = "renderscript.support.mode"; /** The version of the build tools to use to compile */ public static final String PROPERTY_BUILD_TOOLS = "sdk.buildtools"; public static final String PROPERTY_LIBRARY = "android.library"; public static final String PROPERTY_LIB_REF = "android.library.reference."; private static final String PROPERTY_LIB_REF_REGEX = "android.library.reference.\\d+"; public static final String PROPERTY_PROGUARD_CONFIG = "proguard.config"; public static final String PROPERTY_RULES_PATH = "layoutrules.jars"; public static final String PROPERTY_SDK = "sdk.dir"; public static final String PROPERTY_NDK = "ndk.dir"; // LEGACY - Kept so that we can actually remove it from local.properties. private static final String PROPERTY_SDK_LEGACY = "sdk-location"; public static final String PROPERTY_SPLIT_BY_DENSITY = "split.density"; public static final String PROPERTY_SPLIT_BY_ABI = "split.abi"; public static final String PROPERTY_SPLIT_BY_LOCALE = "split.locale"; public static final String PROPERTY_TESTED_PROJECT = "tested.project.dir"; public static final String PROPERTY_BUILD_SOURCE_DIR = "source.dir"; public static final String PROPERTY_BUILD_OUT_DIR = "out.dir"; public static final String PROPERTY_PACKAGE = "package"; public static final String PROPERTY_VERSIONCODE = "versionCode"; public static final String PROPERTY_PROJECTS = "projects"; public static final String PROPERTY_KEY_STORE = "key.store"; public static final String PROPERTY_KEY_ALIAS = "key.alias"; public enum PropertyType { ANT(SdkConstants.FN_ANT_PROPERTIES, BUILD_HEADER, new String[] { PROPERTY_BUILD_SOURCE_DIR, PROPERTY_BUILD_OUT_DIR }, null), PROJECT(SdkConstants.FN_PROJECT_PROPERTIES, DEFAULT_HEADER, new String[] { PROPERTY_TARGET, PROPERTY_LIBRARY, PROPERTY_LIB_REF_REGEX, PROPERTY_KEY_STORE, PROPERTY_KEY_ALIAS, PROPERTY_PROGUARD_CONFIG, PROPERTY_RULES_PATH }, null), LOCAL(SdkConstants.FN_LOCAL_PROPERTIES, LOCAL_HEADER, new String[] { PROPERTY_SDK }, new String[] { PROPERTY_SDK_LEGACY }), @Deprecated LEGACY_DEFAULT("default.properties", null, null, null), @Deprecated LEGACY_BUILD("build.properties", null, null, null); private final String mFilename; private final String mHeader; private final Set<String> mKnownProps; private final Set<String> mRemovedProps; /** * Returns the PropertyTypes ordered the same way Ant order them. */ public static PropertyType[] getOrderedTypes() { return new PropertyType[] { PropertyType.LOCAL, PropertyType.ANT, PropertyType.PROJECT }; } PropertyType(String filename, String header, String[] validProps, String[] removedProps) { mFilename = filename; mHeader = header; HashSet<String> s = new HashSet<String>(); if (validProps != null) { s.addAll(Arrays.asList(validProps)); } mKnownProps = Collections.unmodifiableSet(s); s = new HashSet<String>(); if (removedProps != null) { s.addAll(Arrays.asList(removedProps)); } mRemovedProps = Collections.unmodifiableSet(s); } public String getFilename() { return mFilename; } public String getHeader() { return mHeader; } /** * Returns whether a given property is known for the property type. */ public boolean isKnownProperty(String name) { for (String propRegex : mKnownProps) { if (propRegex.equals(name) || Pattern.matches(propRegex, name)) { return true; } } return false; } /** * Returns whether a given property should be removed for the property type. */ public boolean isRemovedProperty(String name) { for (String propRegex : mRemovedProps) { if (propRegex.equals(name) || Pattern.matches(propRegex, name)) { return true; } } return false; } } private static final String LOCAL_HEADER = // 1-------10--------20--------30--------40--------50--------60--------70--------80 "# This file is automatically generated by Android Tools.\n" + "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" + "#\n" + "# This file must *NOT* be checked into Version Control Systems,\n" + "# as it contains information specific to your local configuration.\n" + "\n"; private static final String DEFAULT_HEADER = // 1-------10--------20--------30--------40--------50--------60--------70--------80 "# This file is automatically generated by Android Tools.\n" + "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" + "#\n" + "# This file must be checked in Version Control Systems.\n" + "#\n" + "# To customize properties used by the Ant build system edit\n" + "# \"ant.properties\", and override values to adapt the script to your\n" + "# project structure.\n" + "#\n" + "# To enable ProGuard to shrink and obfuscate your code, uncomment this " + "(available properties: sdk.dir, user.home):\n" + // Note: always use / separators in the properties paths. Both Ant and // our ExportHelper will convert them properly according to the platform. "#" + PROPERTY_PROGUARD_CONFIG + "=${" + PROPERTY_SDK +"}/" + SdkConstants.FD_TOOLS + '/' + SdkConstants.FD_PROGUARD + '/' + SdkConstants.FN_ANDROID_PROGUARD_FILE + ':' + SdkConstants.FN_PROJECT_PROGUARD_FILE +'\n' + "\n"; private static final String BUILD_HEADER = // 1-------10--------20--------30--------40--------50--------60--------70--------80 "# This file is used to override default values used by the Ant build system.\n" + "#\n" + "# This file must be checked into Version Control Systems, as it is\n" + "# integral to the build system of your project.\n" + "\n" + "# This file is only used by the Ant script.\n" + "\n" + "# You can use this to override default values such as\n" + "# 'source.dir' for the location of your java source folder and\n" + "# 'out.dir' for the location of your output folder.\n" + "\n" + "# You can also use it define how the release builds are signed by declaring\n" + "# the following properties:\n" + "# 'key.store' for the location of your keystore and\n" + "# 'key.alias' for the name of the key to use.\n" + "# The password will be asked during the build when you use the 'release' target.\n" + "\n"; protected final IAbstractFolder mProjectFolder; protected final Map<String, String> mProperties; protected final PropertyType mType; /** * Loads a project properties file and return a {@link ProjectProperties} object * containing the properties. * * @param projectFolderOsPath the project folder. * @param type One the possible {@link PropertyType}s. */ public static ProjectProperties load(String projectFolderOsPath, PropertyType type) { IAbstractFolder wrapper = new FolderWrapper(projectFolderOsPath); return load(wrapper, type); } /** * Loads a project properties file and return a {@link ProjectProperties} object * containing the properties. * * @param projectFolder the project folder. * @param type One the possible {@link PropertyType}s. */ public static ProjectProperties load(IAbstractFolder projectFolder, PropertyType type) { if (projectFolder.exists()) { IAbstractFile propFile = projectFolder.getFile(type.mFilename); if (propFile.exists()) { Map<String, String> map = parsePropertyFile(propFile, null /* log */); if (map != null) { return new ProjectProperties(projectFolder, map, type); } } } return null; } /** * Deletes a project properties file. * * @param projectFolder the project folder. * @param type One the possible {@link PropertyType}s. * @return true if success. */ public static boolean delete(IAbstractFolder projectFolder, PropertyType type) { if (projectFolder.exists()) { IAbstractFile propFile = projectFolder.getFile(type.mFilename); if (propFile.exists()) { return propFile.delete(); } } return false; } /** * Deletes a project properties file. * * @param projectFolderOsPath the project folder. * @param type One the possible {@link PropertyType}s. * @return true if success. */ public static boolean delete(String projectFolderOsPath, PropertyType type) { IAbstractFolder wrapper = new FolderWrapper(projectFolderOsPath); return delete(wrapper, type); } /** * Creates a new project properties object, with no properties. * <p/>The file is not created until {@link ProjectPropertiesWorkingCopy#save()} is called. * @param projectFolderOsPath the project folder. * @param type the type of property file to create * * @see #createEmpty(String, PropertyType) */ public static ProjectPropertiesWorkingCopy create(@NonNull String projectFolderOsPath, @NonNull PropertyType type) { // create and return a ProjectProperties with an empty map. IAbstractFolder folder = new FolderWrapper(projectFolderOsPath); return create(folder, type); } /** * Creates a new project properties object, with no properties. * <p/>The file is not created until {@link ProjectPropertiesWorkingCopy#save()} is called. * @param projectFolder the project folder. * @param type the type of property file to create * * @see #createEmpty(IAbstractFolder, PropertyType) */ public static ProjectPropertiesWorkingCopy create(@NonNull IAbstractFolder projectFolder, @NonNull PropertyType type) { // create and return a ProjectProperties with an empty map. return new ProjectPropertiesWorkingCopy(projectFolder, new HashMap<String, String>(), type); } /** * Creates a new project properties object, with no properties. * <p/>Nothing can be added to it, unless a {@link ProjectPropertiesWorkingCopy} is created * first with {@link #makeWorkingCopy()}. * @param projectFolderOsPath the project folder. * @param type the type of property file to create * * @see #create(String, PropertyType) */ public static ProjectProperties createEmpty(@NonNull String projectFolderOsPath, @NonNull PropertyType type) { // create and return a ProjectProperties with an empty map. IAbstractFolder folder = new FolderWrapper(projectFolderOsPath); return createEmpty(folder, type); } /** * Creates a new project properties object, with no properties. * <p/>Nothing can be added to it, unless a {@link ProjectPropertiesWorkingCopy} is created * first with {@link #makeWorkingCopy()}. * @param projectFolder the project folder. * @param type the type of property file to create * * @see #create(IAbstractFolder, PropertyType) */ public static ProjectProperties createEmpty(@NonNull IAbstractFolder projectFolder, @NonNull PropertyType type) { // create and return a ProjectProperties with an empty map. return new ProjectProperties(projectFolder, new HashMap<String, String>(), type); } /** * Returns the location of this property file. */ public IAbstractFile getFile() { return mProjectFolder.getFile(mType.mFilename); } /** * Creates and returns a copy of the current properties as a * {@link ProjectPropertiesWorkingCopy} that can be modified and saved. * @return a new instance of {@link ProjectPropertiesWorkingCopy} */ public ProjectPropertiesWorkingCopy makeWorkingCopy() { return makeWorkingCopy(mType); } /** * Creates and returns a copy of the current properties as a * {@link ProjectPropertiesWorkingCopy} that can be modified and saved. This also allows * converting to a new type, by specifying a different {@link PropertyType}. * * @param type the {@link PropertyType} of the prop file to save. * * @return a new instance of {@link ProjectPropertiesWorkingCopy} */ public ProjectPropertiesWorkingCopy makeWorkingCopy(PropertyType type) { // copy the current properties in a new map Map<String, String> propList = new HashMap<String, String>(mProperties); return new ProjectPropertiesWorkingCopy(mProjectFolder, propList, type); } /** * Returns the type of the property file. * * @see PropertyType */ public PropertyType getType() { return mType; } /** * Returns the value of a property. * @param name the name of the property. * @return the property value or null if the property is not set. */ @Override public synchronized String getProperty(String name) { return mProperties.get(name); } /** * Returns a set of the property keys. Unlike {@link Map#keySet()} this is not a view of the * map keys. Modifying the returned {@link Set} will not impact the underlying {@link Map}. */ public synchronized Set<String> keySet() { return new HashSet<String>(mProperties.keySet()); } /** * Reloads the properties from the underlying file. */ public synchronized void reload() { if (mProjectFolder.exists()) { IAbstractFile propFile = mProjectFolder.getFile(mType.mFilename); if (propFile.exists()) { Map<String, String> map = parsePropertyFile(propFile, null /* log */); if (map != null) { mProperties.clear(); mProperties.putAll(map); } } } } /** * Parses a property file (using UTF-8 encoding) and returns a map of the content. * <p/> * If the file is not present, null is returned with no error messages sent to the log. * <p/> * IMPORTANT: This method is now unfortunately used in multiple places to parse random * property files. This is NOT a safe practice since there is no corresponding method * to write property files unless you use {@link ProjectPropertiesWorkingCopy#save()}. * Code that writes INI or properties without at least using {@link #escape(String)} will * certainly not load back correct data. <br/> * Unless there's a strong legacy need to support existing files, new callers should * probably just use Java's {@link Properties} which has well defined semantics. * It's also a mistake to write/read property files using this code and expect it to * work with Java's {@link Properties} or external tools (e.g. ant) since there can be * differences in escaping and in character encoding. * * @param propFile the property file to parse * @param log the ILogger object receiving warning/error from the parsing. * @return the map of (key,value) pairs, or null if the parsing failed. */ public static Map<String, String> parsePropertyFile( @NonNull IAbstractFile propFile, @Nullable ILogger log) { InputStream is = null; try { is = propFile.getContents(); return parsePropertyStream(is, propFile.getOsLocation(), log); } catch (StreamException e) { if (log != null) { log.warning("Error parsing '%1$s': %2$s.", propFile.getOsLocation(), e.getMessage()); } } finally { try { Closeables.close(is, true /* swallowIOException */); } catch (IOException e) { // cannot happen } } return null; } /** * Parses a property file (using UTF-8 encoding) and returns a map of the content. * <p/> * Always closes the given input stream on exit. * <p/> * IMPORTANT: This method is now unfortunately used in multiple places to parse random * property files. This is NOT a safe practice since there is no corresponding method * to write property files unless you use {@link ProjectPropertiesWorkingCopy#save()}. * Code that writes INI or properties without at least using {@link #escape(String)} will * certainly not load back correct data. <br/> * Unless there's a strong legacy need to support existing files, new callers should * probably just use Java's {@link Properties} which has well defined semantics. * It's also a mistake to write/read property files using this code and expect it to * work with Java's {@link Properties} or external tools (e.g. ant) since there can be * differences in escaping and in character encoding. * * @param propStream the input stream of the property file to parse. * @param propPath the file path, for display purposed in case of error. * @param log the ILogger object receiving warning/error from the parsing. * @return the map of (key,value) pairs, or null if the parsing failed. */ public static Map<String, String> parsePropertyStream( @NonNull InputStream propStream, @NonNull String propPath, @Nullable ILogger log) { BufferedReader reader = null; try { //noinspection IOResourceOpenedButNotSafelyClosed reader = new BufferedReader( new InputStreamReader(propStream, SdkConstants.INI_CHARSET)); String line = null; Map<String, String> map = new HashMap<String, String>(); while ((line = reader.readLine()) != null) { line = line.trim(); if (!line.isEmpty() && line.charAt(0) != '#') { Matcher m = PATTERN_PROP.matcher(line); if (m.matches()) { map.put(m.group(1), unescape(m.group(2))); } else { if (log != null) { log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", propPath, line); } return null; } } } return map; } catch (FileNotFoundException e) { // this should not happen since we usually test the file existence before // calling the method. // Return null below. } catch (IOException e) { if (log != null) { log.warning("Error parsing '%1$s': %2$s.", propPath, e.getMessage()); } } finally { try { Closeables.close(reader, true /* swallowIOException */); } catch (IOException e) { // cannot happen } try { Closeables.close(propStream, true /* swallowIOException */); } catch (IOException e) { // cannot happen } } return null; } /** * Private constructor. * <p/> * Use {@link #load(String, PropertyType)} or {@link #create(String, PropertyType)} * to instantiate. */ protected ProjectProperties( @NonNull IAbstractFolder projectFolder, @NonNull Map<String, String> map, @NonNull PropertyType type) { mProjectFolder = projectFolder; mProperties = map; mType = type; } private static String unescape(String value) { return value.replaceAll("\\\\\\\\", "\\\\"); } protected static String escape(String value) { return value.replaceAll("\\\\", "\\\\\\\\"); } @Override public void debugPrint() { System.out.println("DEBUG PROJECTPROPERTIES: " + mProjectFolder); System.out.println("type: " + mType); for (Entry<String, String> entry : mProperties.entrySet()) { System.out.println(entry.getKey() + " -> " + entry.getValue()); } System.out.println("<<< DEBUG PROJECTPROPERTIES"); } }