blob: 13341b24ebbb4b03a7500c132a78e53fed052049 [file] [log] [blame]
/*
* Copyright (C) 2014 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.tools.idea.npw;
import com.android.annotations.VisibleForTesting;
import com.android.tools.idea.wizard.WizardConstants;
import com.google.common.base.CharMatcher;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
/**
* Static utility methods used by the New Project/New Module wizards
*/
public class WizardUtils {
private static final CharMatcher ILLEGAL_CHARACTER_MATCHER = CharMatcher.anyOf(WizardConstants.INVALID_FILENAME_CHARS);
/**
* Remove spaces, switch to lower case, and remove any invalid characters. If the resulting name
* conflicts with an existing module, append a number to the end to make a unique name.
*/
@NotNull
public static String computeModuleName(@NotNull String appName, @Nullable Project project) {
String moduleName = appName.toLowerCase().replaceAll(WizardConstants.INVALID_FILENAME_CHARS, "");
moduleName = moduleName.replaceAll("\\s", "");
if (!isUniqueModuleName(moduleName, project)) {
int i = 2;
while (!isUniqueModuleName(moduleName + Integer.toString(i), project)) {
i++;
}
moduleName += Integer.toString(i);
}
return moduleName;
}
/**
* @return true if the given module name is unique inside the given project. Returns true if the given
* project is null.
*/
public static boolean isUniqueModuleName(@NotNull String moduleName, @Nullable Project project) {
if (project == null) {
return true;
}
// Check our modules
ModuleManager moduleManager = ModuleManager.getInstance(project);
for (Module m : moduleManager.getModules()) {
if (m.getName().equalsIgnoreCase(moduleName)) {
return false;
}
}
return true;
}
/**
* Lists the files of the given directory and returns them as an array which
* is never null. This simplifies processing file listings from for each
* loops since {@link File#listFiles} can return null. This method simply
* wraps it and makes sure it returns an empty array instead if necessary.
*
* @param dir the directory to list
* @return the children, or empty if it has no children, is not a directory,
* etc.
*/
@NotNull
public static File[] listFiles(@Nullable File dir) {
if (dir != null) {
File[] files = dir.listFiles();
if (files != null) {
return files;
}
}
return ArrayUtil.EMPTY_FILE_ARRAY;
}
/**
* A Validation Result for Wizard Validations, contains a status and a message
*/
public static class ValidationResult {
public enum Status {
OK, WARN, ERROR
}
public enum Message {
NO_LOCATION_SPECIFIED("Please specify a %1s"),
BAD_SLASHES("Your %1s contains incorrect slashes ('\\' vs '/')"),
ILLEGAL_CHARACTER("Illegal character in %1s path: '%2c' in filename %3s"),
ILLEGAL_FILENAME("Illegal filename in %1s path: %2s"),
WHITESPACE("Your %1s contains whitespace. This can cause problems on some platforms and is not recommended."),
NON_ASCII_CHARS("Your %1s contains non-ASCII characters. This can cause problems on Windows. Proceed with caution."),
PATH_NOT_WRITEABLE("The path '%2s' is not writeable. Please choose a new location."),
PROJECT_LOC_IS_FILE("There must not already be a file at the %1s."),
NON_EMPTY_DIR(
"A non-empty directory already exists at the specified %1s. Existing files may be overwritten. Proceed with caution."),
PROJECT_IS_FILE_SYSTEM_ROOT("The %1s can not be at the filesystem root"),
IS_UNDER_ANDROID_STUDIO_ROOT("Path points to a location within Android Studio installation directory"),
PARENT_NOT_DIR("The %1s's parent directory must be a directory, not a plain file"),
INSIDE_ANDROID_STUDIO("The %1s is inside %2s install location");
private final String myText;
Message(final String text) {
myText = text;
}
@Override
public String toString() {
return myText;
}
}
public static final ValidationResult OK = new ValidationResult(Status.OK, null, "any");
private final Status myStatus;
private final Message myMessage;
private final Object[] myMessageParams;
private ValidationResult(@NotNull Status status, @Nullable Message message, @NotNull String field, Object... messageParams) {
myStatus = status;
myMessage = message;
myMessageParams = ArrayUtil.prepend(field, messageParams);
}
public static ValidationResult warn(@NotNull Message message, String field, Object... params) {
return new ValidationResult(Status.WARN, message, field, params);
}
public static ValidationResult error(@NotNull Message message, String field, Object... params) {
return new ValidationResult(Status.ERROR, message, field, params);
}
@Nullable
@VisibleForTesting
Message getMessage() {
return myMessage;
}
@VisibleForTesting
Object[] getMessageParams() {
return myMessageParams;
}
public String getFormattedMessage() {
if(myMessage == null) {
throw new IllegalStateException("Null message, are you trying to get the message of an OK?");
}
return String.format(myMessage.toString(), myMessageParams);
}
@NotNull
public Status getStatus() {
return myStatus;
}
public boolean isError() {
return myStatus.equals(Status.ERROR);
}
public boolean isOk() {
return myStatus.equals(Status.OK);
}
}
/**
* Will return {@link WizardUtils.ValidationResult#OK} if projectLocation is valid
* or {@link WizardUtils.ValidationResult} with error if not.
*/
@NotNull
public static ValidationResult validateLocation(@Nullable String projectLocation) {
return validateLocation(projectLocation, "project location", true);
}
@NotNull
public static ValidationResult validateLocation(@Nullable String projectLocation, @NotNull String fieldName, boolean checkEmpty) {
ValidationResult warningResult = null;
if (projectLocation == null || projectLocation.isEmpty()) {
return ValidationResult.error(ValidationResult.Message.NO_LOCATION_SPECIFIED, fieldName);
}
// Check the separators
if ((File.separatorChar == '/' && projectLocation.contains("\\")) ||
(File.separatorChar == '\\' && projectLocation.contains("/"))) {
return ValidationResult.error(ValidationResult.Message.BAD_SLASHES, fieldName);
}
// Check the individual components for not allowed characters.
File testFile = new File(projectLocation);
while (testFile != null) {
String filename = testFile.getName();
if (ILLEGAL_CHARACTER_MATCHER.matchesAnyOf(filename)) {
char illegalChar = filename.charAt(ILLEGAL_CHARACTER_MATCHER.indexIn(filename));
return ValidationResult.error(ValidationResult.Message.ILLEGAL_CHARACTER, fieldName, illegalChar, filename);
}
if (WizardConstants.INVALID_WINDOWS_FILENAMES.contains(filename.toLowerCase())) {
return ValidationResult.error(ValidationResult.Message.ILLEGAL_FILENAME, fieldName, filename);
}
if (CharMatcher.WHITESPACE.matchesAnyOf(filename)) {
warningResult = ValidationResult.warn(ValidationResult.Message.WHITESPACE, fieldName);
}
if (!CharMatcher.ASCII.matchesAllOf(filename)) {
warningResult = ValidationResult.warn(ValidationResult.Message.NON_ASCII_CHARS, fieldName);
}
// Check that we can write to that location: make sure we can write into the first extant directory in the path.
if (!testFile.exists() && testFile.getParentFile() != null && testFile.getParentFile().exists()) {
if (!testFile.getParentFile().canWrite()) {
return ValidationResult.error(ValidationResult.Message.PATH_NOT_WRITEABLE, fieldName, testFile.getParentFile().getPath());
}
}
testFile = testFile.getParentFile();
}
File file = new File(projectLocation);
if (file.isFile()) {
return ValidationResult.error(ValidationResult.Message.PROJECT_LOC_IS_FILE, fieldName);
}
if (file.getParent() == null) {
return ValidationResult.error(ValidationResult.Message.PROJECT_IS_FILE_SYSTEM_ROOT, fieldName);
}
if (file.getParentFile().exists() && !file.getParentFile().isDirectory()) {
return ValidationResult.error(ValidationResult.Message.PARENT_NOT_DIR, fieldName);
}
if (file.exists() && !file.canWrite()) {
return ValidationResult.error(ValidationResult.Message.PATH_NOT_WRITEABLE, fieldName, file.getPath());
}
String installLocation = PathManager.getHomePathFor(Application.class);
if (installLocation != null && FileUtil.isAncestor(new File(installLocation), file, false)) {
String applicationName = ApplicationNamesInfo.getInstance().getProductName();
return ValidationResult.error(ValidationResult.Message.INSIDE_ANDROID_STUDIO, fieldName, applicationName);
}
if (checkEmpty && file.exists() && listFiles(file).length > 0) {
return ValidationResult.warn(ValidationResult.Message.NON_EMPTY_DIR, fieldName);
}
return (warningResult == null) ? ValidationResult.OK : warningResult;
}
}