com.android.build.gradle.internal.pipeline.IntermediateFolderUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.android.build.gradle.internal.pipeline.IntermediateFolderUtils.java

Source

/*
 * Copyright (C) 2015 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.build.gradle.internal.pipeline;

import static com.android.SdkConstants.DOT_JAR;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent.ContentType;
import com.android.build.api.transform.QualifiedContent.Scope;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.TransformInput;
import com.android.utils.FileUtils;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.tasks.incremental.InputFileDetails;

import java.io.File;
import java.util.List;
import java.util.Set;

/**
 * Helper to handle the folder structure in the output of transforms.
 */
public class IntermediateFolderUtils {

    public static final String FOLDERS = "folders";
    public static final String JARS = "jars";

    /**
     * Returns the location of content for a given set of Scopes, Content Types, and format.
     *
     * If the format is {@link Format#DIRECTORY} then the result is the file location of the folder.
     * If the format is {@link Format#JAR} then the result is a file representing the jar to create.
     *
     * @param rootLocation the root location from which to create the content.
     * @param name a unique name for the content. For a given set of scopes/types/format it must
     *             be unique.
     * @param types the content types associated with this content.
     * @param scopes the scopes associated with this content.
     * @param format the format of the content.
     * @return the location of the content.
     */
    @NonNull
    public static File getContentLocation(@NonNull File rootLocation, @NonNull String name,
            @NonNull Set<ContentType> types, @NonNull Set<Scope> scopes, @NonNull Format format) {
        // runtime check these since it's (indirectly) called by 3rd party transforms.
        checkNotNull(name);
        checkNotNull(types);
        checkNotNull(scopes);
        checkNotNull(format);
        checkState(!name.isEmpty());
        checkState(!types.isEmpty());
        checkState(!scopes.isEmpty());

        switch (format) {
        case DIRECTORY: {
            File location = FileUtils.join(rootLocation, FOLDERS, typesToString(types), scopesToString(scopes),
                    name);
            return location;
        }
        case JAR: {
            File location = FileUtils.join(rootLocation, JARS, typesToString(types), scopesToString(scopes),
                    name + DOT_JAR);
            return location;
        }
        default:
            throw new RuntimeException("Unexpected Format: " + format);
        }
    }

    @NonNull
    public static TransformInput computeNonIncrementalInputFromFolder(@NonNull File folder,
            @NonNull Set<ContentType> requiredTypes, @NonNull Set<Scope> requiredScopes) {
        final List<JarInput> jarInputs = Lists.newArrayList();
        final List<DirectoryInput> directoryInputs = Lists.newArrayList();

        File jarsFolder = new File(folder, JARS);
        if (jarsFolder.isDirectory()) {
            parseTypeLevelFolders(jarsFolder, requiredTypes, requiredScopes, new InputGenerator() {
                @Override
                public boolean accept(@NonNull File file) {
                    return file.isFile() && file.getName().endsWith(DOT_JAR);
                }

                @Override
                public void generate(@NonNull File file, @NonNull Set<ContentType> types,
                        @NonNull Set<Scope> scopes) {
                    jarInputs.add(new ImmutableJarInput(
                            file.getName().substring(0, file.getName().length() - DOT_JAR.length()), file,
                            Status.NOTCHANGED, types, scopes));

                }
            });
        }

        File foldersFolder = new File(folder, FOLDERS);
        if (foldersFolder.isDirectory()) {
            parseTypeLevelFolders(foldersFolder, requiredTypes, requiredScopes, new InputGenerator() {
                @Override
                public boolean accept(@NonNull File file) {
                    return file.isDirectory();
                }

                @Override
                public void generate(@NonNull File file, @NonNull Set<ContentType> types,
                        @NonNull Set<Scope> scopes) {
                    directoryInputs.add(new ImmutableDirectoryInput(file.getName(), file, types, scopes));
                }
            });
        }

        return new ImmutableTransformInput(jarInputs, directoryInputs, folder);
    }

    static class IntermediateTransformInput extends IncrementalTransformInput {

        @NonNull
        private final File inputRoot;
        private List<String> rootLocationSegments = null;

        IntermediateTransformInput(@NonNull File inputRoot) {
            this.inputRoot = inputRoot;
        }

        @Override
        protected boolean checkRemovedFolder(@NonNull Set<Scope> transformScopes,
                @NonNull Set<ContentType> transformInputTypes, @NonNull File file,
                @NonNull List<String> fileSegments) {
            if (!checkRootSegments(fileSegments)) {
                return false;
            }

            // there must be at least 5 additional segments (4 to the root of the folder and 1 for
            // the file inside.
            if (fileSegments.size() <= rootLocationSegments.size() + 4) {
                return false;
            }

            // now check that the segments after the root are what we expect.
            int index = rootLocationSegments.size();
            if (!fileSegments.get(index++).equals(FOLDERS)) {
                return false;
            }

            // get the types.
            Set<ContentType> types = stringToTypes(fileSegments.get(index++));
            if (types == null) {
                return false;
            }

            // if the transform is not interested in this folder type, we don't care about those
            // changes
            if (Sets.intersection(transformInputTypes, types).isEmpty()) {
                return true;
            }

            // get the scopes.
            Set<Scope> scopes = stringToScopes(fileSegments.get(index++));
            if (scopes == null) {
                return false;
            }

            // if the scopes do not match the transform scope, we don't care about those changes.
            if (Sets.intersection(transformScopes, scopes).isEmpty()) {
                return true;
            }

            String name = fileSegments.get(index);

            // create the folder input. a mutable one so that it can be directly used
            // for other removed files from the same folder.
            // The root location of this folder is fileSegments, up to rootLocation + 4 (as the
            // rest is the changed file.
            File root = new File(FileUtils.join(fileSegments.subList(0, rootLocationSegments.size() + 4)));
            MutableDirectoryInput folder = new MutableDirectoryInput(name, root, types, scopes);
            // add this file to it.
            Logging.getLogger(TransformManager.class).info("Tagged" + file.getAbsolutePath() + " as removed");
            folder.addChangedFile(file, Status.REMOVED);

            // add it to the list.
            addFolderInput(folder);

            return true;
        }

        @Override
        boolean checkRemovedJarFile(@NonNull Set<Scope> transformScopes,
                @NonNull Set<ContentType> transformInputTypes, @NonNull File file,
                @NonNull List<String> fileSegments) {
            if (!checkRootSegments(fileSegments)) {
                return false;
            }

            // there must be only 4 additional segments.
            if (fileSegments.size() != rootLocationSegments.size() + 4) {
                return false;
            }

            // last segment must end in .jar
            if (!file.getPath().endsWith(DOT_JAR)) {
                return false;
            }

            // now check that the segments after the root are what we expect.
            int index = rootLocationSegments.size();
            if (!fileSegments.get(index++).equals(JARS)) {
                return false;
            }

            // get the types.
            Set<ContentType> types = stringToTypes(fileSegments.get(index++));
            if (types == null) {
                return false;
            }
            // if the transform is not interested in this jar type, we don't care about those
            // changes
            if (Sets.intersection(transformInputTypes, types).isEmpty()) {
                return true;
            }

            // get the scopes.
            Set<Scope> scopes = stringToScopes(fileSegments.get(index++));
            if (scopes == null) {
                return false;
            }

            // if the scopes do not match the transform scope, we don't care about those changes.
            if (Sets.intersection(transformScopes, scopes).isEmpty()) {
                return true;
            }

            String name = fileSegments.get(index);

            // create the jar input
            addImmutableJar(new ImmutableJarInput(name, file, Status.REMOVED, types, scopes));

            return true;
        }

        private boolean checkRootSegments(@NonNull List<String> fileSegments) {
            if (rootLocationSegments == null) {
                rootLocationSegments = Lists
                        .newArrayList(Splitter.on(File.separatorChar).split(inputRoot.getAbsolutePath()));
            }

            if (fileSegments.size() <= rootLocationSegments.size()) {
                return false;
            }

            // compare segments going backward as the leafs are more likely to be different.
            // We can ignore the segments of the file that are beyond the segments for the folder.
            for (int i = rootLocationSegments.size() - 1; i >= 0; i--) {
                if (!rootLocationSegments.get(i).equals(fileSegments.get(i))) {
                    return false;
                }
            }

            return true;
        }

    }

    @NonNull
    public static IncrementalTransformInput computeIncrementalInputFromFolder(@NonNull File rootLocation,
            @NonNull Set<ContentType> requiredTypes, @NonNull Set<Scope> requiredScopes) {
        final IncrementalTransformInput input = new IntermediateTransformInput(rootLocation);

        File jarsFolder = new File(rootLocation, JARS);
        if (jarsFolder.isDirectory()) {
            parseTypeLevelFolders(jarsFolder, requiredTypes, requiredScopes, new InputGenerator() {
                @Override
                public boolean accept(@NonNull File file) {
                    return file.isFile() && file.getName().endsWith(DOT_JAR);
                }

                @Override
                public void generate(@NonNull File file, @NonNull Set<ContentType> types,
                        @NonNull Set<Scope> scopes) {
                    input.addJarInput(new QualifiedContentImpl(
                            file.getName().substring(0, file.getName().length() - DOT_JAR.length()), file, types,
                            scopes) {
                    });

                }
            });
        }

        File foldersFolder = new File(rootLocation, FOLDERS);
        if (foldersFolder.isDirectory()) {
            parseTypeLevelFolders(foldersFolder, requiredTypes, requiredScopes, new InputGenerator() {
                @Override
                public boolean accept(@NonNull File file) {
                    return file.isDirectory();
                }

                @Override
                public void generate(@NonNull File file, @NonNull Set<ContentType> types,
                        @NonNull Set<Scope> scopes) {
                    input.addFolderInput(new MutableDirectoryInput(file.getName(), file, types, scopes));
                }
            });
        }

        return input;
    }

    private interface InputGenerator {
        boolean accept(@NonNull File file);

        void generate(@NonNull File file, @NonNull Set<ContentType> types, @NonNull Set<Scope> scopes);
    }

    private static void parseTypeLevelFolders(@NonNull File rootFolder, @NonNull Set<ContentType> requiredTypes,
            @NonNull Set<Scope> requiredScopes, @NonNull InputGenerator generator) {
        File[] files = rootFolder.listFiles(File::isDirectory);

        if (files != null && files.length > 0) {
            for (File file : files) {
                Set<ContentType> types = stringToTypes(file.getName());
                if (types != null) {
                    // check these are types we care about and only pass down types we care about.
                    // In this case we can safely return the content with a limited type,
                    // as file extension allows for differentiation.
                    Set<ContentType> limitedTypes = Sets.intersection(requiredTypes, types);
                    if (!limitedTypes.isEmpty()) {
                        parseScopeLevelFolders(file, limitedTypes, requiredScopes, generator);
                    }
                }
            }
        }
    }

    private static void parseScopeLevelFolders(@NonNull File rootFolder, @NonNull Set<ContentType> types,
            @NonNull Set<Scope> requiredScopes, @NonNull InputGenerator generator) {
        File[] files = rootFolder.listFiles(File::isDirectory);

        if (files != null && files.length > 0) {
            for (File file : files) {
                Set<Scope> scopes = stringToScopes(file.getName());
                if (scopes != null) {
                    // we need up to the requiredScopes, but no more.
                    // content that only contains unwanted Scope can be safely dropped, however
                    // content that is both in and out of Scope will trigger a runtime error.
                    // check these are the scope we want, and only pass down scopes we care about.
                    Set<Scope> limitedScopes = Sets.newHashSetWithExpectedSize(requiredScopes.size());
                    boolean foundUnwanted = false;
                    for (Scope scope : scopes) {
                        if (requiredScopes.contains(scope)) {
                            limitedScopes.add(scope);
                        } else {
                            foundUnwanted = true;
                        }
                    }
                    if (!limitedScopes.isEmpty()) {
                        if (foundUnwanted) {
                            throw new RuntimeException("error");
                        }
                        parseContentLevelFolders(file, types, Sets.immutableEnumSet(limitedScopes), generator);
                    }
                }
            }
        }
    }

    private static void parseContentLevelFolders(@NonNull File rootFolder, @NonNull Set<ContentType> types,
            @NonNull Set<Scope> scopes, @NonNull final InputGenerator generator) {

        File[] files = rootFolder.listFiles(generator::accept);

        if (files != null && files.length > 0) {
            for (File file : files) {
                generator.generate(file, types, scopes);
            }
        }
    }

    @Nullable
    private static Set<ContentType> stringToTypes(String folderName) {
        int value;
        try {
            value = Integer.parseInt(folderName, 16);
        } catch (NumberFormatException e) {
            return null;
        }

        ImmutableSet.Builder<ContentType> typesBuilder = ImmutableSet.builder();

        for (ContentType type : ExtendedContentType.getAllContentTypes()) {
            if ((type.getValue() & value) != 0) {
                typesBuilder.add(type);
            }
        }

        Set<ContentType> types = typesBuilder.build();
        if (types.isEmpty()) {
            return null;
        }

        return types;
    }

    private static String typesToString(@NonNull Set<ContentType> types) {
        int value = 0;
        for (ContentType type : types) {
            value += type.getValue();
        }

        return String.format("%x", value);
    }

    @Nullable
    private static Set<Scope> stringToScopes(String folderName) {
        int value;
        try {
            value = Integer.parseInt(folderName, 16);
        } catch (NumberFormatException e) {
            return null;
        }

        Set<Scope> scopes = Sets.newHashSet();

        for (Scope scope : Scope.values()) {
            if ((scope.getValue() & value) != 0) {
                scopes.add(scope);
            }
        }

        if (scopes.isEmpty()) {
            return null;
        }

        return Sets.immutableEnumSet(scopes);
    }

    private static String scopesToString(@NonNull Set<Scope> scopes) {
        int value = 0;
        for (Scope scope : scopes) {
            value += scope.getValue();
        }

        return String.format("%x", value);
    }

    @NonNull
    static Status inputFileDetailsToStatus(@NonNull InputFileDetails inputFileDetails) {
        if (inputFileDetails.isAdded())
            return Status.ADDED;
        if (inputFileDetails.isModified())
            return Status.CHANGED;
        if (inputFileDetails.isRemoved())
            return Status.REMOVED;
        return Status.NOTCHANGED;
    }
}