/* * Copyright (C) 2013 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.templates; import com.android.SdkConstants; import com.android.builder.model.SourceProvider; import com.android.ide.common.res2.ResourceItem; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.tools.idea.rendering.AppResourceRepository; import com.android.tools.idea.rendering.ResourceFolderRegistry; import com.android.tools.idea.rendering.ResourceFolderRepository; import com.android.tools.idea.rendering.ResourceNameValidator; import com.google.common.base.Splitter; import com.google.common.collect.Sets; import com.intellij.openapi.diagnostic.Logger; 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.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.psi.search.GlobalSearchScope; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidRootUtil; import org.jetbrains.android.facet.IdeaSourceProvider; import org.jetbrains.android.util.AndroidUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.w3c.dom.Element; import java.io.File; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import static com.android.tools.idea.templates.Template.*; /** * Parameter represents an external input to a template. It consists of an ID used to refer to it within the template, * human-readable information to be displayed in the UI, and type and validation specifications that can be used in the UI to assist in * data entry. */ public class Parameter { private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.templates.Parameter"); private static final Set<String> typeValues = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); public enum Type { STRING, BOOLEAN, ENUM, SEPARATOR, EXTERNAL, CUSTOM; // TODO: Numbers? public static Type get(String name) { try { if (typeValues.isEmpty()) { for(Type t : Type.values()) { typeValues.add(t.name().toUpperCase(Locale.US)); } } String upperCaseName = name.toUpperCase(Locale.US); if (!typeValues.contains(upperCaseName)) { return Type.CUSTOM; } return Type.valueOf(upperCaseName); } catch (IllegalArgumentException e) { LOG.error("Unexpected template type '" + name + "'"); LOG.error("Expected one of :"); for (Type s : Type.values()) { LOG.error(" " + s.name().toLowerCase(Locale.US)); } } return STRING; } } /** * Constraints that can be applied to a parameter which helps the UI add a * validator etc for user input. These are typically combined into a set * of constraints via an EnumSet. */ public enum Constraint { /** * This value must be unique. This constraint usually only makes sense * when other constraints are specified, such as {@link #LAYOUT}, which * means that the parameter should designate a name that does not * represent an existing layout resource name */ UNIQUE, /** * This value must already exist. This constraint usually only makes sense * when other constraints are specified, such as {@link #LAYOUT}, which * means that the parameter should designate a name that already exists as * a resource name. */ EXISTS, /** The associated value must not be empty */ NONEMPTY, /** The associated value is allowed to be empty */ EMPTY, /** The associated value should represent a fully qualified activity class name */ ACTIVITY, /** The associated value should represent an API level */ APILEVEL, /** The associated value should represent a valid class name */ CLASS, /** The associated value should represent a valid package name */ PACKAGE, /** The associated value should represent a valid Android application package name */ APP_PACKAGE, /** The associated value should represent a valid Module name */ MODULE, /** The associated value should represent a valid layout resource name */ LAYOUT, /** The associated value should represent a valid drawable resource name */ DRAWABLE, /** The associated value should represent a valid id resource name */ ID, /** The associated value should represent a valid source directory name */ SOURCE_SET_FOLDER, /** The associated value should represent a valid string resource name */ STRING; public static Constraint get(String name) { try { return Constraint.valueOf(name.toUpperCase(Locale.US)); } catch (IllegalArgumentException e) { LOG.error("Unexpected template constraint '" + name + "'"); if (name.indexOf(',') != -1) { LOG.error("Use | to separate constraints"); } else { LOG.error("Expected one of :"); for (Constraint s : Constraint.values()) { LOG.error(" " + s.name().toLowerCase(Locale.US)); } } } return NONEMPTY; } } /** The template defining the parameter */ public final TemplateMetadata template; /** The type of parameter */ @NotNull public final Type type; /** The unique id of the parameter (not displayed to the user) */ @Nullable public final String id; /** The display name for this parameter */ @Nullable public final String name; /** * The initial value for this parameter (see also {@link #suggest} for more * dynamic defaults */ @Nullable public final String initial; /** * A template expression using other template parameters for producing a * default value based on other edited parameters, if possible. */ @Nullable public final String suggest; /** * A template expression using other template parameters for dynamically changing * the visibility of this parameter to the user. */ @Nullable public final String visibility; /** * A URL for externally sourced values. */ @Nullable public final String sourceUrl; /** Help for the parameter, if any */ @Nullable public final String help; /** The element defining this parameter */ @NotNull public final Element element; /** The constraints applicable for this parameter */ @NotNull public final EnumSet<Constraint> constraints; /** The dsl name of the type that will be created in the ui for the user to enter this parameter. * This should correspond to a name registered by an ExternalWizardParameterFactory extension. */ public String externalTypeName; Parameter(@NotNull TemplateMetadata template, @NotNull Element parameter) { this.template = template; element = parameter; String typeName = parameter.getAttribute(Template.ATTR_TYPE); assert typeName != null && !typeName.isEmpty() : Template.ATTR_TYPE; type = Type.get(typeName); id = parameter.getAttribute(ATTR_ID); initial = parameter.getAttribute(ATTR_DEFAULT); suggest = parameter.getAttribute(ATTR_SUGGEST); visibility = parameter.getAttribute(ATTR_VISIBILITY); sourceUrl = type == Type.EXTERNAL ? parameter.getAttribute(ATTR_SOURCE_URL) : null; name = parameter.getAttribute(ATTR_NAME); help = parameter.getAttribute(ATTR_HELP); if (type == Type.CUSTOM) { externalTypeName = typeName; } else { externalTypeName = null; } String constraintString = parameter.getAttribute(ATTR_CONSTRAINTS); if (constraintString != null && !constraintString.isEmpty()) { EnumSet<Constraint> constraintSet = null; for (String s : Splitter.on('|').omitEmptyStrings().split(constraintString)) { Constraint constraint = Constraint.get(s); if (constraintSet == null) { constraintSet = EnumSet.of(constraint); } else { constraintSet = EnumSet.copyOf(constraintSet); constraintSet.add(constraint); } } constraints = constraintSet; } else { constraints = EnumSet.noneOf(Constraint.class); } } Parameter( @NotNull TemplateMetadata template, @NotNull Type type, @NotNull String id) { this.template = template; this.type = type; this.id = id; element = null; initial = null; suggest = null; visibility = null; sourceUrl = null; name = id; help = null; constraints = EnumSet.noneOf(Constraint.class); } public List<Element> getOptions() { return TemplateUtils.getChildren(element); } @Nullable public String validate(@Nullable Project project, @Nullable String packageName, @Nullable Object value) { return validate(project, null, null, packageName, value); } @Nullable public String validate(@Nullable Project project, @Nullable Module module, @Nullable String packageName, @Nullable Object value) { return validate(project, module, null, packageName, value); } @Nullable public String validate(@Nullable Project project, @Nullable Module module, @Nullable SourceProvider provider, @Nullable String packageName, @Nullable Object value) { switch (type) { case EXTERNAL: case CUSTOM: case STRING: return getErrorMessageForStringType(project, module, provider, packageName, value.toString()); case BOOLEAN: case ENUM: case SEPARATOR: default: return null; } } /** * Validate the given value for this parameter and list any reasons why the given value is invalid. * @param project * @param packageName * @param value * @return An error message detailing why the given value is invalid. */ @Nullable protected String getErrorMessageForStringType(@Nullable Project project, @Nullable Module module, @Nullable SourceProvider provider, @Nullable String packageName, @Nullable String value) { Collection<Constraint> violations = validateStringType(project, module, provider, packageName, value); if (violations.contains(Constraint.NONEMPTY)) { return "Please specify " + name; } else if (violations.contains(Constraint.ACTIVITY)) { return name + " is not a valid activity name"; } else if (violations.contains(Constraint.APILEVEL)) { // TODO: validity check } else if (violations.contains(Constraint.CLASS)) { return name + " is not a valid class name"; } else if (violations.contains(Constraint.PACKAGE)) { return name + " is not a valid package name"; } else if (violations.contains(Constraint.MODULE)) { return name + " is not a valid module name"; } else if (violations.contains(Constraint.APP_PACKAGE) && value != null) { String message = AndroidUtils.validateAndroidPackageName(value); if (message != null) { return message; } } else if (violations.contains(Constraint.LAYOUT) && value != null) { String resourceNameError = ResourceNameValidator.create(false, ResourceFolderType.LAYOUT).getErrorText(value); if (resourceNameError != null) { return name + " is not a valid resource name. " + resourceNameError; } } else if (violations.contains(Constraint.DRAWABLE)) { String resourceNameError = ResourceNameValidator.create(false, ResourceFolderType.DRAWABLE).getErrorText(value); if (resourceNameError != null) { return name + " is not a valid resource name. " + resourceNameError; } } else if (violations.contains(Constraint.ID)) { return name + " is not a valid id."; } else if (violations.contains(Constraint.STRING)) { String resourceNameError = ResourceNameValidator.create(false, ResourceFolderType.VALUES).getErrorText(value); if (resourceNameError != null) { return name + " is not a valid resource name. " + resourceNameError; } } if (violations.contains(Constraint.UNIQUE)) { return name + " must be unique"; } else if (violations.contains(Constraint.EXISTS)) { return name + " must already exist"; } return null; } /** * Validate the given value for this parameter and list the constraints that the given value violates. * @return All constraints of this parameter that are violated by the proposed value. */ @NotNull protected Collection<Constraint> validateStringType(@Nullable Project project, @Nullable Module module, @Nullable SourceProvider provider, @Nullable String packageName, @Nullable String value) { GlobalSearchScope searchScope = module != null ? GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module) : GlobalSearchScope.EMPTY_SCOPE; Set<Constraint> violations = Sets.newHashSet(); if (value == null || value.isEmpty()) { if (constraints.contains(Constraint.NONEMPTY)) { violations.add(Constraint.NONEMPTY); } return violations; } boolean exists = false; String fqName = (packageName != null && value.indexOf('.') == -1 ? packageName + "." : "") + value; if (constraints.contains(Constraint.ACTIVITY)) { if (!isValidFullyQualifiedJavaIdentifier(fqName)) { violations.add(Constraint.ACTIVITY); } if (project != null) { PsiClass aClass = JavaPsiFacade.getInstance(project).findClass(fqName, searchScope); PsiClass activityClass = JavaPsiFacade.getInstance(project).findClass(SdkConstants.CLASS_ACTIVITY, GlobalSearchScope.allScope(project)); exists = aClass != null && activityClass != null && aClass.isInheritor(activityClass, true); } } if (constraints.contains(Constraint.APILEVEL)) { // TODO: validity check } if (constraints.contains(Constraint.CLASS)) { if (!isValidFullyQualifiedJavaIdentifier(fqName)) { violations.add(Constraint.CLASS); } if (project != null) { exists = existsClassFile(project, searchScope, provider, fqName); } } if (constraints.contains(Constraint.PACKAGE)) { if (!isValidFullyQualifiedJavaIdentifier(value)) { violations.add(Constraint.PACKAGE); } if (project != null) { exists = existsPackage(project, searchScope, provider, value); } } if (constraints.contains(Constraint.MODULE)) { // TODO: validity check if (project != null) { exists = ModuleManager.getInstance(project).findModuleByName(value) != null; } } if (constraints.contains(Constraint.APP_PACKAGE)) { String message = AndroidUtils.validateAndroidPackageName(value); if (message != null) { violations.add(Constraint.APP_PACKAGE); } if (project != null) { exists = existsPackage(project, searchScope, provider, value); } } if (constraints.contains(Constraint.LAYOUT)) { String resourceNameError = ResourceNameValidator.create(false, ResourceFolderType.LAYOUT).getErrorText(value); if (resourceNameError != null) { violations.add(Constraint.LAYOUT); } exists = provider != null ? existsResourceFile(provider, module, ResourceFolderType.LAYOUT, ResourceType.LAYOUT, value) : existsResourceFile(module, ResourceType.LAYOUT, value); } if (constraints.contains(Constraint.DRAWABLE)) { String resourceNameError = ResourceNameValidator.create(false, ResourceFolderType.DRAWABLE).getErrorText(value); if (resourceNameError != null) { violations.add(Constraint.DRAWABLE); } exists = provider != null ? existsResourceFile(provider, module, ResourceFolderType.DRAWABLE, ResourceType.DRAWABLE, value) : existsResourceFile(module, ResourceType.DRAWABLE, value); } if (constraints.contains(Constraint.ID)) { // TODO: validity and existence check } if (constraints.contains(Constraint.STRING)) { String resourceNameError = ResourceNameValidator.create(false, ResourceFolderType.VALUES).getErrorText(value); if (resourceNameError != null) { violations.add(Constraint.STRING); } // TODO: Existence check } if (constraints.contains(Constraint.SOURCE_SET_FOLDER)) { if (module != null) { AndroidFacet facet = AndroidFacet.getInstance(module); if (facet != null) { String modulePath = AndroidRootUtil.getModuleDirPath(module); if (modulePath != null) { File file = new File(FileUtil.toSystemDependentName(modulePath), value); VirtualFile vFile = VfsUtil.findFileByIoFile(file, true); exists = !IdeaSourceProvider.getSourceProvidersForFile(facet, vFile, null).isEmpty(); } } } } if (constraints.contains(Constraint.UNIQUE) && exists) { violations.add(Constraint.UNIQUE); } else if (constraints.contains(Constraint.EXISTS) && !exists) { violations.add(Constraint.EXISTS); } return violations; } /** * Returns true if the given stringType is non-unique when it should be. */ public boolean uniquenessSatisfied(@Nullable Project project, @Nullable Module module, @Nullable SourceProvider provider, @Nullable String packageName, @Nullable String value) { return !validateStringType(project, module, provider, packageName, value).contains(Constraint.UNIQUE); } private static boolean isValidFullyQualifiedJavaIdentifier(String value) { return AndroidUtils.isValidJavaPackageName(value) && value.indexOf('.') != -1; } public static boolean existsResourceFile(@Nullable Module module, @NotNull ResourceType resourceType, @Nullable String name) { if (name == null || name.isEmpty() || module == null) { return false; } AndroidFacet facet = AndroidFacet.getInstance(module); if (facet != null) { AppResourceRepository repository = facet.getAppResources(true); return repository.hasResourceItem(resourceType, name); } return false; } public static boolean existsResourceFile(@Nullable SourceProvider sourceProvider, @Nullable Module module, @NotNull ResourceFolderType resourceFolderType, @NotNull ResourceType resourceType, @Nullable String name) { if (name == null || name.isEmpty() || sourceProvider == null) { return false; } AndroidFacet facet = module != null ? AndroidFacet.getInstance(module) : null; for (File resDir : sourceProvider.getResDirectories()) { if (facet != null) { VirtualFile virtualResDir = VfsUtil.findFileByIoFile(resDir, false); if (virtualResDir != null) { ResourceFolderRepository folderRepository = ResourceFolderRegistry.get(facet, virtualResDir); List<ResourceItem> resourceItemList = folderRepository.getResourceItem(resourceType, name); if (resourceItemList != null && !resourceItemList.isEmpty()) { return true; } } } else if (existsResourceFile(resDir, resourceFolderType, name)) { return true; } } return false; } public static boolean existsResourceFile(File resDir, ResourceFolderType resourceType, String name) { File[] resTypes = resDir.listFiles(); if (resTypes != null) { for (File resTypeDir : resTypes) { if (resTypeDir.isDirectory() && resourceType.equals(ResourceFolderType.getFolderType(resTypeDir.getName()))) { File[] files = resTypeDir.listFiles(); if (files != null) { for (File f : files) { if (getNameWithoutExtensions(f).equalsIgnoreCase(name)) { return true; } } } } } } return false; } @NotNull private static String getNameWithoutExtensions(@NotNull File f) { if (f.getName().indexOf('.') == -1) { return f.getName(); } else { return f.getName().substring(0, f.getName().indexOf('.')); } } public static boolean existsClassFile(@Nullable Project project, @NotNull GlobalSearchScope searchScope, @Nullable SourceProvider sourceProvider, @NotNull String fullyQualifiedClassName) { if (project == null) { return false; } if (sourceProvider != null) { for (File javaDir : sourceProvider.getJavaDirectories()) { File classFile = new File(javaDir, fullyQualifiedClassName.replace('.', File.separatorChar) + SdkConstants.DOT_JAVA); if (classFile.exists()) { return true; } } return false; } else if (searchScope != GlobalSearchScope.EMPTY_SCOPE) { return JavaPsiFacade.getInstance(project).findClass(fullyQualifiedClassName, searchScope) != null; } else { return false; } } public static boolean existsPackage(@Nullable Project project, @NotNull GlobalSearchScope searchScope, @Nullable SourceProvider sourceProvider, @NotNull String packageName) { if (project == null) { return false; } if (sourceProvider != null) { for (File javaDir : sourceProvider.getJavaDirectories()) { File classFile = new File(javaDir, packageName.replace('.', File.separatorChar)); if (classFile.exists() && classFile.isDirectory()) { return true; } } return false; } else { return JavaPsiFacade.getInstance(project).findPackage(packageName) != null; } } @Override public String toString() { return "(parameter id: " + id + ")"; } }