Java tutorial
/* * 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.google.gct.idea.appengine.action; import com.android.tools.idea.gradle.parser.BuildFileKey; import com.android.tools.idea.gradle.parser.BuildFileStatement; import com.android.tools.idea.gradle.parser.Dependency; import com.android.tools.idea.gradle.parser.GradleBuildFile; import com.android.tools.idea.gradle.project.GradleProjectImporter; import com.android.tools.idea.gradle.util.GradleUtil; import com.android.tools.idea.stats.UsageTracker; import com.android.tools.idea.templates.Parameter; import com.android.tools.idea.templates.Template; import com.android.tools.idea.templates.TemplateMetadata; import com.android.tools.idea.templates.TemplateUtils; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gct.idea.appengine.dom.WebApp; import com.google.gct.idea.appengine.gradle.facet.AppEngineGradleFacet; import com.google.gct.idea.appengine.util.AppEngineUtils; import com.google.gct.idea.appengine.util.PsiUtils; import com.google.gct.idea.appengine.wizard.CloudModuleUtils; import com.google.gct.idea.appengine.wizard.CloudTemplateUtils; import com.google.gct.idea.util.GctTracking; import com.intellij.codeInsight.AnnotationUtil; import com.intellij.codeInsight.actions.ReformatCodeProcessor; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.source.PsiJavaFileImpl; import com.intellij.util.ArrayUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; /** * Action to generate an Endpoint class from a specified class. */ public class NewEndpointFromClassAction extends AnAction { private static final Logger LOG = Logger.getInstance(NewEndpointFromClassAction.class); private static final String COMPILE_DIRTY_ACTION_ID = "CompileDirty"; private static final String ENTITY_NAME = "entityName"; private static final String ENTITY_TYPE = "entityType"; private static final String ENDPOINT_TEMPLATE = "EndpointFromClass"; private static final String ENDPOINT_CLASS_SUFFIX = "Endpoint"; private static final String ERROR_MESSAGE_TITLE = "Failed to Generate Endpoint Class"; private static final String DEFAULT_ERROR_MESSAGE = "Error occurred while generating Endpoint class"; private static final String ENDPOINTS_DEPENDENCY = "com.google.appengine:appengine-endpoints:"; private static final String ENDPOINTS_DEPS_DEPENDENCY = "com.google.appengine:appengine-endpoints-deps:"; private static final String ENDPOINTS_SERVLET_CLASS = "com.google.api.server.spi.SystemServiceServlet"; private static final String ENDOINTS_SERVLET_NAME = "SystemServiceServlet"; private static final String ENDPOINTS_SERVICES_INIT_PARAM_NAME = "services"; private static final String OBJECTIFY_ENTITY_ANNOTATION = "com.googlecode.objectify.annotation.Entity"; private static final String OBJECTIFY_ID_ANNOTATION = "com.googlecode.objectify.annotation.Id"; private static final String OBJECTIFY_FILTER_NAME = "ObjectifyFilter"; private static final String OBJECTIFY_FQ_FILTER_CLASS = "com.googlecode.objectify.ObjectifyFilter"; @Override public void update(AnActionEvent e) { if (ActionPlaces.isPopupPlace(e.getPlace())) { e.getPresentation().setVisible(shouldDisplayAction(e)); } } private static boolean shouldDisplayAction(AnActionEvent e) { if (!AppEngineUtils.isAppEngineModule(e.getData(LangDataKeys.MODULE))) { return false; } PsiFile psiFile = e.getData(LangDataKeys.PSI_FILE); if (psiFile == null || !(psiFile instanceof PsiJavaFileImpl)) { return false; } Module srcModule = ModuleUtilCore.findModuleForPsiElement(psiFile); if (srcModule == null) { return false; } if (ProjectRootManager.getInstance(srcModule.getProject()).getFileIndex() .getSourceRootForFile(psiFile.getVirtualFile()) == null) { return false; } return true; } @Override public void actionPerformed(AnActionEvent e) { PsiJavaFile psiJavaFile = PsiUtils.getPsiJavaFileFromContext(e); Project project = e.getProject(); Module module = e.getData(LangDataKeys.MODULE); if (psiJavaFile == null || module == null || project == null) { Messages.showErrorDialog(project, "Please select a Java file to create an Endpoint for.", ERROR_MESSAGE_TITLE); return; } if (!AppEngineUtils.isAppEngineModule(module)) { Messages.showErrorDialog(project, "Endpoints can only be generated for App Engine projects.", ERROR_MESSAGE_TITLE); return; } String packageName = psiJavaFile.getPackageName(); PsiDirectory psiJavaFileContainingDirectory = psiJavaFile.getContainingDirectory(); if (psiJavaFileContainingDirectory == null) { Messages.showErrorDialog(project, DEFAULT_ERROR_MESSAGE, ERROR_MESSAGE_TITLE); } String directory = psiJavaFileContainingDirectory.getVirtualFile().getPath(); PsiClass[] psiClasses = psiJavaFile.getClasses(); if (psiClasses.length > 1) { Messages.showErrorDialog(project, "We only support generating an endpoint from Java files with one top level class.", "Error Generating Endpoint"); return; } if (psiClasses.length == 0) { Messages.showErrorDialog(project, "This Java file does not contain any classes.", "Error Generating Endpoint"); return; } PsiClass resourcePsiClass = psiClasses[0]; boolean isObjectifyEntity = AnnotationUtil.isAnnotated(resourcePsiClass, OBJECTIFY_ENTITY_ANNOTATION, true); String idType = null; String idName = null; String idGetterName = null; if (isObjectifyEntity) { for (PsiField psiField : resourcePsiClass.getAllFields()) { if (AnnotationUtil.isAnnotated(psiField, OBJECTIFY_ID_ANNOTATION, false)) { idType = psiField.getType().getPresentableText(); idName = psiField.getName(); } } if (idType == null) { Messages.showErrorDialog(project, "Please add the required @Id annotation to your entity before trying to generate an endpoint from" + " this class.", "Error Generating Objectify Endpoint."); return; } idGetterName = getIdGetter(resourcePsiClass, idName); } String fileName = psiJavaFile.getName(); String classType = fileName.substring(0, fileName.lastIndexOf('.')); doAction(project, module, packageName, directory, classType, isObjectifyEntity, idType, idName, idGetterName); AnAction compileDirty = ActionManager.getInstance().getAction(COMPILE_DIRTY_ACTION_ID); if (compileDirty != null) { compileDirty.actionPerformed(e); } } @Nullable private static String getIdGetter(@NotNull PsiClass resourcePsiClass, @NotNull String idName) { PsiMethod[] idGetterMethods = resourcePsiClass.findMethodsByName("get" + StringUtil.capitalize(idName), true); if (idGetterMethods.length == 0) { return null; } for (PsiMethod idGetterMethod : idGetterMethods) { if (idGetterMethod.getParameterList().getParametersCount() == 0) { return idGetterMethod.getName(); } } return null; } /** * Generates an endpoint class in the specified module and updates the gradle build file * to include endpoint dependencies if they don't already exist. */ private static void doAction(Project project, Module module, String packageName, String directory, @NonNls String classType, boolean isObjectifyEntity, @Nullable String idType, @Nullable String idName, @Nullable String idGetterName) { if (classType.isEmpty()) { Messages.showErrorDialog(project, "Class object is required for Endpoint generation", ERROR_MESSAGE_TITLE); return; } // Check if there is a file with the same name as the file that will contain the endpoint class String endpointFileName = directory + "/" + classType + ENDPOINT_CLASS_SUFFIX + ".java"; String endpointFQClassName = packageName + "." + classType + ENDPOINT_CLASS_SUFFIX; File temp = new File(endpointFileName); if (temp.exists()) { Messages.showErrorDialog(project, "\'" + temp.getName() + "\" already exists", ERROR_MESSAGE_TITLE); return; } CloudTemplateUtils.TemplateInfo templateInfo = CloudTemplateUtils.getTemplate(ENDPOINT_TEMPLATE); if (templateInfo == null) { LOG.error("Failed to load endpoint template info: " + ENDPOINT_TEMPLATE); Messages.showErrorDialog(project, DEFAULT_ERROR_MESSAGE, ERROR_MESSAGE_TITLE); return; } final Template template = Template.createFromPath(templateInfo.getFile()); final File projectRoot = new File(project.getBasePath()); final File moduleRoot = new File(projectRoot, module.getName()); // Create the replacement map final Map<String, Object> replacementMap = Maps.newHashMap(); try { replacementMap.put(TemplateMetadata.ATTR_PROJECT_OUT, moduleRoot.getCanonicalPath()); } catch (IOException e) { LOG.error(e); Messages.showErrorDialog("Failed to resolve Module output destination : " + e.getMessage(), ERROR_MESSAGE_TITLE); return; } String className = String.valueOf(Character.toLowerCase(classType.charAt(0))); if (classType.length() > 1) { className += classType.substring(1); } replacementMap.put("isObjectified", isObjectifyEntity); replacementMap.put("idType", idType); replacementMap.put("idName", idName); replacementMap.put("idGetterName", idGetterName); replacementMap.put(ENTITY_NAME, className); replacementMap.put(ENTITY_TYPE, classType); replacementMap.put(TemplateMetadata.ATTR_SRC_DIR, directory); replacementMap.put(TemplateMetadata.ATTR_PACKAGE_NAME, packageName); // Owner Domain is the reverse of package path. replacementMap.put(CloudModuleUtils.ATTR_ENDPOINTS_OWNER, StringUtil.join(ArrayUtil.reverseArray(packageName.split("\\.")), ".")); replacementMap.put(CloudModuleUtils.ATTR_ENDPOINTS_PACKAGE, ""); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { template.render(projectRoot, moduleRoot, replacementMap); } }); // Add any missing Endpoint dependency to the build file and sync updateBuildFile(project, module, template); updateWebXml(project, module, endpointFQClassName, isObjectifyEntity); // Open the new Endpoint class in the editor VirtualFile endpointFile = LocalFileSystem.getInstance().findFileByPath(endpointFileName); new ReformatCodeProcessor(project, PsiManager.getInstance(project).findFile(endpointFile), null, false) .run(); TemplateUtils.openEditor(project, endpointFile); UsageTracker.getInstance().trackEvent(GctTracking.CATEGORY, GctTracking.NEW_ENDPOINT, isObjectifyEntity ? "objectify" : "pojo", null); } private static void updateWebXml(Project project, Module module, final String endpointFQClassName, boolean isObjectifyEndpoint) { AppEngineGradleFacet facet = AppEngineGradleFacet.getAppEngineFacetByModule(module); final WebApp webApp = facet.getWebXmlForEdit(); if (isObjectifyEndpoint && !hasObjectifyFilter(webApp)) { CommandProcessor.getInstance().executeCommand(project, new Runnable() { @Override public void run() { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { WebApp.Filter objectifyFilter = webApp.addFilter(); WebApp.FilterMapping filterMapping = webApp.addFilterMapping(); objectifyFilter.getFilterName().setValue(OBJECTIFY_FILTER_NAME); objectifyFilter.getFilterClass().setValue(OBJECTIFY_FQ_FILTER_CLASS); filterMapping.getFilterName().setValue(OBJECTIFY_FILTER_NAME); filterMapping.getUrlPattern().setValue("/*"); } }); } }, "Update App Engine web.xml", null); } String result = validateWebXml(webApp); if (result != null) { Messages.showErrorDialog(result, ERROR_MESSAGE_TITLE); return; } WebApp.Servlet.InitParam.ParamValue endpointServletParamValue = getEndpointServletInitParam(webApp); if (endpointServletParamValue == null) { Messages.showErrorDialog( "Could not find a correctly configured SystemServiceServlet in this module's web.xml", ERROR_MESSAGE_TITLE); } else { addEndpointClassToInitParam(project, endpointServletParamValue, endpointFQClassName); } } private static boolean hasObjectifyFilter(WebApp webApp) { for (WebApp.Filter filter : webApp.getFilters()) { if (filter.getFilterName().getStringValue().trim().equals(OBJECTIFY_FILTER_NAME)) { return true; } } return false; } @Nullable private static WebApp.Servlet.InitParam.ParamValue getEndpointServletInitParam(@NotNull WebApp webApp) { for (WebApp.Servlet servlet : webApp.getServlets()) { String servletName = servlet.getServletName().getValue(); String servletClass = servlet.getServletClass().getValue(); if (servletName == null || servletClass == null) { continue; } if (servletName.equals(ENDOINTS_SERVLET_NAME) && servletClass.equals(ENDPOINTS_SERVLET_CLASS)) { if (servlet.getInitParams() == null) { continue; } for (WebApp.Servlet.InitParam initParam : servlet.getInitParams()) { String paramName = initParam.getParamName().getValue(); if (paramName != null && paramName.equals(ENDPOINTS_SERVICES_INIT_PARAM_NAME)) { return initParam.getParamValue(); } } } } return null; } @Nullable private static String validateWebXml(WebApp webApp) { if (webApp == null) { return "This App Engine module's web.xml could not be updated as the file failed to load."; } if (webApp.getServlets() == null) { return "This App Engine module's web.xml does not contain the required SystemServiceServlet servlet."; } return null; } private static void addEndpointClassToInitParam(@NotNull Project project, @NotNull final WebApp.Servlet.InitParam.ParamValue initParamValue, @NotNull final String endpointFQClassName) { if (initParamValue.getValue().contains(endpointFQClassName)) { return; } final String initParamValueString = initParamValue.getValue().trim().isEmpty() ? endpointFQClassName : initParamValue.getValue() + ", " + endpointFQClassName; CommandProcessor.getInstance().executeCommand(project, new Runnable() { @Override public void run() { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { initParamValue.setValue(initParamValueString); } }); } }, "Update App Engine web.xml", null); } /** * Adds missing endpoint dependencies to gradle build file of the specified module. */ private static void updateBuildFile(@NonNls Project project, @NotNull Module module, @NonNls Template template) { final VirtualFile buildFile = GradleUtil.getGradleBuildFile(module); if (buildFile == null) { Messages.showErrorDialog("Cannot find gradle build file for module \"" + module.getName() + "\"", ERROR_MESSAGE_TITLE); return; } Parameter appEngineVersionParam = template.getMetadata().getParameter("appEngineVersion"); String appEngineVersion = (appEngineVersionParam == null) ? "+" : appEngineVersionParam.initial; final GradleBuildFile gradleBuildFile = new GradleBuildFile(buildFile, project); Dependency endpointDependency = new Dependency(Dependency.Scope.COMPILE, Dependency.Type.EXTERNAL, ENDPOINTS_DEPENDENCY + appEngineVersion); Dependency endpointDepsDependency = new Dependency(Dependency.Scope.COMPILE, Dependency.Type.EXTERNAL, ENDPOINTS_DEPS_DEPENDENCY + appEngineVersion); List<Dependency> missingDependencies = Lists.newArrayList(); // Check if the endpoint dependencies already exist in the gradle file if (!gradleBuildFile.hasDependency(endpointDependency)) { missingDependencies.add(endpointDependency); } if (!gradleBuildFile.hasDependency(endpointDepsDependency)) { missingDependencies.add(endpointDepsDependency); } // Add the missing dependencies to the gradle build file if (!missingDependencies.isEmpty()) { final List<BuildFileStatement> currentDependencies = gradleBuildFile.getDependencies(); currentDependencies.addAll(missingDependencies); WriteCommandAction.runWriteCommandAction(project, new Runnable() { @Override public void run() { gradleBuildFile.setValue(BuildFileKey.DEPENDENCIES, currentDependencies); } }); GradleProjectImporter.getInstance().requestProjectSync(project, null); } } }