blob: c685e1d023dbb16d03cbd64f40a620618fab2133 [file] [log] [blame]
/*
* 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 static 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");
}
}