| /* |
| * 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.ProjectViewStorageManager; |
| import com.google.idea.blaze.base.projectview.ProjectViewVerifier; |
| import com.google.idea.blaze.base.projectview.section.ScalarSection; |
| import com.google.idea.blaze.base.projectview.section.sections.ImportSection; |
| 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.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 final JPanel component; |
| private final String buildSystemName; |
| private final ProjectViewUi projectViewUi; |
| |
| private TextFieldWithBrowseButton projectDataDirField; |
| private JTextField projectNameField; |
| private HashCode paramsHash; |
| private WorkspaceRoot workspaceRoot; |
| |
| 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(); |
| |
| HashCode hashCode = |
| Hashing.md5() |
| .newHasher() |
| .putUnencodedChars(workspaceName) |
| .putUnencodedChars(workspaceRoot.toString()) |
| .putUnencodedChars(workspacePath != null ? workspacePath.toString() : "") |
| .putUnencodedChars(initialProjectViewText != null ? initialProjectViewText : "") |
| .hash(); |
| |
| // If any params have changed, reinit the control |
| if (!hashCode.equals(paramsHash)) { |
| this.paramsHash = hashCode; |
| init( |
| workspaceName, |
| workspaceRoot, |
| workspaceOption.getWorkspacePathResolver(), |
| workspacePath, |
| initialProjectViewText); |
| } |
| } |
| |
| private void init( |
| String workspaceName, |
| WorkspaceRoot workspaceRoot, |
| WorkspacePathResolver workspacePathResolver, |
| @Nullable WorkspacePath sharedProjectView, |
| @Nullable String initialProjectViewText) { |
| this.workspaceRoot = workspaceRoot; |
| 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(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 ProjectViewSet projectViewSet; |
| |
| private boolean success; |
| List<IssueOutput> errors = Lists.newArrayList(); |
| |
| ProjectViewValidator(ProjectViewSet projectViewSet) { |
| 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.verifyProjectViewNoDisk( |
| context, 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); |
| } |
| } |