com.google.idea.blaze.base.wizard2.ui.BlazeEditProjectViewControl.java Source code

Java tutorial

Introduction

Here is the source code for com.google.idea.blaze.base.wizard2.ui.BlazeEditProjectViewControl.java

Source

/*
 * Copyright 2016 The Bazel Authors. All rights reserved.
 *
 * 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.google.idea.blaze.base.wizard2.ui;

import com.google.common.collect.Lists;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.idea.blaze.base.model.primitives.WorkspacePath;
import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
import com.google.idea.blaze.base.projectview.ProjectView;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.projectview.ProjectViewSet.ProjectViewFile;
import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
import com.google.idea.blaze.base.projectview.ProjectViewVerifier;
import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
import com.google.idea.blaze.base.projectview.section.ScalarSection;
import com.google.idea.blaze.base.projectview.section.SectionParser;
import com.google.idea.blaze.base.projectview.section.sections.ImportSection;
import com.google.idea.blaze.base.projectview.section.sections.Sections;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.scope.OutputSink.Propagation;
import com.google.idea.blaze.base.scope.Scope;
import com.google.idea.blaze.base.scope.output.IssueOutput;
import com.google.idea.blaze.base.scope.output.IssueOutput.Category;
import com.google.idea.blaze.base.settings.ui.JPanelProvidingProject;
import com.google.idea.blaze.base.settings.ui.ProjectViewUi;
import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
import com.google.idea.blaze.base.ui.BlazeValidationError;
import com.google.idea.blaze.base.ui.BlazeValidationResult;
import com.google.idea.blaze.base.ui.UiUtil;
import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
import com.google.idea.blaze.base.wizard2.BlazeSelectProjectViewOption;
import com.google.idea.blaze.base.wizard2.BlazeSelectWorkspaceOption;
import com.google.idea.blaze.base.wizard2.ProjectDataDirectoryValidator;
import com.google.idea.common.experiments.BoolExperiment;
import com.intellij.ide.RecentProjectsManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.ui.TextComponentAccessor;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.SystemProperties;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/** The UI control to collect project settings when importing a Blaze project. */
public final class BlazeEditProjectViewControl {

    private static final FileChooserDescriptor PROJECT_FOLDER_DESCRIPTOR = new FileChooserDescriptor(false, true,
            false, false, false, false);
    private static final Logger LOG = Logger.getInstance(BlazeEditProjectViewControl.class);

    private static final BoolExperiment allowAddprojectViewDefaultValues = new BoolExperiment(
            "allow.add.project.view.default.values", true);

    private final JPanel component;
    private final String buildSystemName;
    private final ProjectViewUi projectViewUi;

    private TextFieldWithBrowseButton projectDataDirField;
    private JTextField projectNameField;
    private HashCode paramsHash;
    private WorkspaceRoot workspaceRoot;
    private WorkspacePathResolver workspacePathResolver;

    public BlazeEditProjectViewControl(BlazeNewProjectBuilder builder, Disposable parentDisposable) {
        this.projectViewUi = new ProjectViewUi(parentDisposable);
        JPanel component = new JPanelProvidingProject(ProjectViewUi.getProject(), new GridBagLayout());
        fillUi(component, 0);
        update(builder);
        UiUtil.fillBottom(component);
        this.component = component;
        this.buildSystemName = builder.getBuildSystemName();
    }

    public Component getUiComponent() {
        return component;
    }

    private void fillUi(JPanel canvas, int indentLevel) {
        JLabel projectDataDirLabel = new JBLabel("Project data directory:");

        Dimension minSize = ProjectViewUi.getMinimumSize();
        // Add 120 pixels so we have room for our extra fields
        minSize.setSize(minSize.width, minSize.height + 120);
        canvas.setMinimumSize(minSize);
        canvas.setPreferredSize(minSize);

        projectDataDirField = new TextFieldWithBrowseButton();
        projectDataDirField.addBrowseFolderListener("", buildSystemName + " project data directory", null,
                PROJECT_FOLDER_DESCRIPTOR, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, false);
        final String dataDirToolTipText = "Directory in which to store the project's metadata. "
                + "Choose a directory outside of your workspace.";
        projectDataDirField.setToolTipText(dataDirToolTipText);
        projectDataDirLabel.setToolTipText(dataDirToolTipText);

        canvas.add(projectDataDirLabel, UiUtil.getLabelConstraints(indentLevel));
        canvas.add(projectDataDirField, UiUtil.getFillLineConstraints(0));

        JLabel projectNameLabel = new JLabel("Project name:");
        projectNameField = new JTextField();
        final String projectNameToolTipText = "Project display name.";
        projectNameField.setToolTipText(projectNameToolTipText);
        projectNameLabel.setToolTipText(projectNameToolTipText);
        canvas.add(projectNameLabel, UiUtil.getLabelConstraints(indentLevel));
        canvas.add(projectNameField, UiUtil.getFillLineConstraints(0));

        projectViewUi.fillUi(canvas, indentLevel);
    }

    public void update(BlazeNewProjectBuilder builder) {
        BlazeSelectWorkspaceOption workspaceOption = builder.getWorkspaceOption();
        BlazeSelectProjectViewOption projectViewOption = builder.getProjectViewOption();
        String workspaceName = workspaceOption.getWorkspaceName();
        WorkspaceRoot workspaceRoot = workspaceOption.getWorkspaceRoot();
        WorkspacePath workspacePath = projectViewOption.getSharedProjectView();
        String initialProjectViewText = projectViewOption.getInitialProjectViewText();
        boolean allowAddDefaultValues = projectViewOption.allowAddDefaultProjectViewValues()
                && allowAddprojectViewDefaultValues.getValue();
        WorkspacePathResolver workspacePathResolver = workspaceOption.getWorkspacePathResolver();

        HashCode hashCode = Hashing.md5().newHasher().putUnencodedChars(workspaceName)
                .putUnencodedChars(workspaceRoot.toString())
                .putUnencodedChars(workspacePath != null ? workspacePath.toString() : "")
                .putUnencodedChars(initialProjectViewText != null ? initialProjectViewText : "")
                .putBoolean(allowAddDefaultValues).hash();

        // If any params have changed, reinit the control
        if (!hashCode.equals(paramsHash)) {
            this.paramsHash = hashCode;
            init(workspaceName, workspaceRoot, workspacePathResolver, workspacePath, initialProjectViewText,
                    allowAddDefaultValues);
        }
    }

    private static String modifyInitialProjectView(String initialProjectViewText,
            WorkspacePathResolver workspacePathResolver) {
        BlazeContext context = new BlazeContext();
        ProjectViewParser projectViewParser = new ProjectViewParser(context, workspacePathResolver);
        projectViewParser.parseProjectView(initialProjectViewText);
        ProjectViewSet projectViewSet = projectViewParser.getResult();
        ProjectViewFile projectViewFile = projectViewSet.getTopLevelProjectViewFile();
        if (projectViewFile == null) {
            return initialProjectViewText;
        }
        ProjectView projectView = projectViewFile.projectView;
        for (SectionParser sectionParser : Sections.getParsers()) {
            projectView = sectionParser.addProjectViewDefaultValue(projectView);
        }
        return ProjectViewParser.projectViewToString(projectView);
    }

    private void init(String workspaceName, WorkspaceRoot workspaceRoot,
            WorkspacePathResolver workspacePathResolver, @Nullable WorkspacePath sharedProjectView,
            @Nullable String initialProjectViewText, boolean allowAddDefaultValues) {
        if (allowAddDefaultValues && initialProjectViewText != null) {
            initialProjectViewText = modifyInitialProjectView(initialProjectViewText, workspacePathResolver);
        }

        this.workspaceRoot = workspaceRoot;
        this.workspacePathResolver = workspacePathResolver;
        projectNameField.setText(workspaceName);
        String defaultDataDir = getDefaultProjectDataDirectory(workspaceName);
        projectDataDirField.setText(defaultDataDir);

        String projectViewText = "";
        File sharedProjectViewFile = null;

        if (sharedProjectView != null) {
            sharedProjectViewFile = workspacePathResolver.resolveToFile(sharedProjectView);

            try {
                projectViewText = ProjectViewStorageManager.getInstance().loadProjectView(sharedProjectViewFile);
                if (projectViewText == null) {
                    LOG.error("Could not load project view: " + sharedProjectViewFile);
                    projectViewText = "";
                }
            } catch (IOException e) {
                LOG.error(e);
            }
        } else {
            projectViewText = initialProjectViewText;
            LOG.assertTrue(projectViewText != null);
        }

        projectViewUi.init(workspacePathResolver, projectViewText,
                sharedProjectView != null ? projectViewText : null, sharedProjectView, sharedProjectView != null,
                false /* allowEditShared - not allowed during import */);
    }

    private static String getDefaultProjectDataDirectory(String projectName) {
        File defaultDataDirectory = new File(getDefaultProjectsDirectory());
        File desiredLocation = new File(defaultDataDirectory, projectName);
        return newUniquePath(desiredLocation);
    }

    private static String getDefaultProjectsDirectory() {
        final String lastProjectLocation = RecentProjectsManager.getInstance().getLastProjectCreationLocation();
        if (lastProjectLocation != null) {
            return lastProjectLocation.replace('/', File.separatorChar);
        }
        final String userHome = SystemProperties.getUserHome();
        String productName = ApplicationNamesInfo.getInstance().getLowercaseProductName();
        return userHome.replace('/', File.separatorChar) + File.separator + productName.replace(" ", "")
                + "Projects";
    }

    /** Returns a unique file path by appending numbers until a non-collision is found. */
    private static String newUniquePath(File location) {
        if (!location.exists()) {
            return location.getAbsolutePath();
        }

        String name = location.getName();
        File directory = location.getParentFile();
        int tries = 0;
        while (true) {
            String candidateName = String.format("%s-%02d", name, tries);
            File candidateFile = new File(directory, candidateName);
            if (!candidateFile.exists()) {
                return candidateFile.getAbsolutePath();
            }
            tries++;
        }
    }

    public BlazeValidationResult validate() {
        // Validate project settings fields
        String projectName = projectNameField.getText().trim();
        if (StringUtil.isEmpty(projectName)) {
            return BlazeValidationResult.failure(new BlazeValidationError("Project name is not specified"));
        }
        String projectDataDirPath = projectDataDirField.getText().trim();
        if (StringUtil.isEmpty(projectDataDirPath)) {
            return BlazeValidationResult
                    .failure(new BlazeValidationError("Project data directory is not specified"));
        }
        File projectDataDir = new File(projectDataDirPath);
        if (!projectDataDir.isAbsolute()) {
            return BlazeValidationResult.failure(new BlazeValidationError("Project data directory is not valid"));
        }
        for (ProjectDataDirectoryValidator validator : ProjectDataDirectoryValidator.EP_NAME.getExtensions()) {
            BlazeValidationResult result = validator.validateDataDirectory(projectDataDir);
            if (!result.success) {
                return result;
            }
        }
        File workspaceRootDirectory = workspaceRoot.directory();
        if (FileUtil.isAncestor(projectDataDir, workspaceRootDirectory, false)) {
            return BlazeValidationResult
                    .failure(new BlazeValidationError("Project data directory must not contain the workspace. "
                            + "Please choose a directory outside your workspace."));
        }
        if (FileUtil.isAncestor(workspaceRootDirectory, projectDataDir, false)) {
            return BlazeValidationResult
                    .failure(new BlazeValidationError("Project data directory cannot be inside your workspace. "
                            + "Please choose a directory outside your workspace."));
        }

        List<IssueOutput> issues = Lists.newArrayList();

        ProjectViewSet projectViewSet = projectViewUi.parseProjectView(issues);
        BlazeValidationError projectViewParseError = validationErrorFromIssueList(issues);
        if (projectViewParseError != null) {
            return BlazeValidationResult.failure(projectViewParseError);
        }

        ProjectViewValidator projectViewValidator = new ProjectViewValidator(workspacePathResolver, projectViewSet);
        ProgressManager.getInstance().runProcessWithProgressSynchronously(projectViewValidator,
                "Validating Project", false, null);

        if (!projectViewValidator.success) {
            if (!projectViewValidator.errors.isEmpty()) {
                return BlazeValidationResult.failure(validationErrorFromIssueList(projectViewValidator.errors));
            }
            return BlazeValidationResult
                    .failure("Project view validation failed, but we couldn't find an error message. "
                            + "Please report a bug.");
        }

        return BlazeValidationResult.success();
    }

    private static class ProjectViewValidator implements Runnable {
        private final WorkspacePathResolver workspacePathResolver;
        private final ProjectViewSet projectViewSet;

        private boolean success;
        List<IssueOutput> errors = Lists.newArrayList();

        ProjectViewValidator(WorkspacePathResolver workspacePathResolver, ProjectViewSet projectViewSet) {
            this.workspacePathResolver = workspacePathResolver;
            this.projectViewSet = projectViewSet;
        }

        @Override
        public void run() {
            success = Scope.root(this::validateProjectView);
        }

        @NotNull
        private Boolean validateProjectView(BlazeContext context) {
            context.addOutputSink(IssueOutput.class, output -> {
                if (output.getCategory() == Category.ERROR) {
                    errors.add(output);
                }
                return Propagation.Continue;
            });
            for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
                syncPlugin.installSdks(context);
            }
            WorkspaceLanguageSettings workspaceLanguageSettings = LanguageSupport
                    .createWorkspaceLanguageSettings(context, projectViewSet);
            if (workspaceLanguageSettings == null) {
                return false;
            }
            return ProjectViewVerifier.verifyProjectView(context, workspacePathResolver, projectViewSet,
                    workspaceLanguageSettings);
        }
    }

    @Nullable
    private static BlazeValidationError validationErrorFromIssueList(List<IssueOutput> issues) {
        List<IssueOutput> errors = issues.stream()
                .filter(issue -> issue.getCategory() == IssueOutput.Category.ERROR).collect(Collectors.toList());

        if (!errors.isEmpty()) {
            StringBuilder errorMessage = new StringBuilder();
            errorMessage.append("The following issues were found:\n\n");
            for (IssueOutput issue : errors) {
                errorMessage.append(issue.getMessage());
                errorMessage.append('\n');
            }
            return new BlazeValidationError(errorMessage.toString());
        }
        return null;
    }

    public void updateBuilder(BlazeNewProjectBuilder builder) {
        String projectName = projectNameField.getText().trim();
        String projectDataDirectory = projectDataDirField.getText().trim();
        File localProjectViewFile = ProjectViewStorageManager.getLocalProjectViewFileName(builder.getBuildSystem(),
                new File(projectDataDirectory));

        BlazeSelectProjectViewOption selectProjectViewOption = builder.getProjectViewOption();
        boolean useSharedProjectView = projectViewUi.getUseSharedProjectView();

        // If we're using a shared project view, synthesize a local one that imports the shared one
        ProjectViewSet parseResult = projectViewUi.parseProjectView(Lists.newArrayList());

        final ProjectView projectView;
        final ProjectViewSet projectViewSet;
        if (useSharedProjectView && selectProjectViewOption.getSharedProjectView() != null) {
            projectView = ProjectView.builder().add(
                    ScalarSection.builder(ImportSection.KEY).set(selectProjectViewOption.getSharedProjectView()))
                    .build();
            projectViewSet = ProjectViewSet.builder().addAll(parseResult.getProjectViewFiles())
                    .add(localProjectViewFile, projectView).build();
        } else {
            ProjectViewSet.ProjectViewFile projectViewFile = parseResult.getTopLevelProjectViewFile();
            assert projectViewFile != null;
            projectView = projectViewFile.projectView;
            projectViewSet = parseResult;
        }

        builder.setProjectView(projectView).setProjectViewFile(localProjectViewFile)
                .setProjectViewSet(projectViewSet).setProjectName(projectName)
                .setProjectDataDirectory(projectDataDirectory);
    }
}