aboutsummaryrefslogtreecommitdiffstats
path: root/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java
blob: d2a5fdb6b32f7400cf81ce7952509f13356e7d2a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
/*
 * 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.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 final static Pattern PATTERN_PROP = Pattern.compile(
    "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$");

    /** The property name for the project target */
    public final static String PROPERTY_TARGET = "target";

    public final static String PROPERTY_LIBRARY = "android.library";
    public final static String PROPERTY_LIB_REF = "android.library.reference.";
    private final static String PROPERTY_LIB_REF_REGEX = "android.library.reference.\\d+";

    public final static String PROPERTY_PROGUARD_CONFIG = "proguard.config";
    public final static String PROPERTY_RULES_PATH = "layoutrules.jars";

    public final static String PROPERTY_SDK = "sdk.dir";
    // LEGACY - Kept so that we can actually remove it from local.properties.
    private final static String PROPERTY_SDK_LEGACY = "sdk-location";

    public final static String PROPERTY_SPLIT_BY_DENSITY = "split.density";
    public final static String PROPERTY_SPLIT_BY_ABI = "split.abi";
    public final static String PROPERTY_SPLIT_BY_LOCALE = "split.locale";

    public final static String PROPERTY_TESTED_PROJECT = "tested.project.dir";

    public final static String PROPERTY_BUILD_SOURCE_DIR = "source.dir";
    public final static String PROPERTY_BUILD_OUT_DIR = "out.dir";

    public final static String PROPERTY_PACKAGE = "package";
    public final static String PROPERTY_VERSIONCODE = "versionCode";
    public final static String PROPERTY_PROJECTS = "projects";
    public final static String PROPERTY_KEY_STORE = "key.store";
    public final static 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 final static 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 final static 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 final static 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) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(propFile.getContents(),
                    SdkConstants.INI_CHARSET));

            String line = null;
            Map<String, String> map = new HashMap<String, String>();
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.length() > 0 && 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",
                                    propFile.getOsLocation(),
                                    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.",
                        propFile.getOsLocation(),
                        e.getMessage());
            }
        } catch (StreamException e) {
            if (log != null) {
                log.warning("Error parsing '%1$s': %2$s.",
                        propFile.getOsLocation(),
                        e.getMessage());
            }
        } finally {
            Closeables.closeQuietly(reader);
        }

        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");

    }

}