/* * Copyright 2015 The authors * 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.intellij.struts2.jsp.inspection; import com.intellij.codeInsight.completion.ExtendedTagInsertHandler; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.codeInspection.XmlSuppressableInspectionTool; import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.html.HtmlTag; import com.intellij.psi.jsp.JspFile; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlTag; import com.intellij.struts2.StrutsBundle; import com.intellij.struts2.StrutsConstants; import com.intellij.struts2.facet.StrutsFacet; import com.intellij.struts2.model.constant.StrutsConstantHelper; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.ContainerUtil; import com.intellij.xml.XmlNamespaceHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.net.URL; import java.util.Collections; /* * @author max * @author Yann Cébron */ public class HardcodedActionUrlInspection extends XmlSuppressableInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) { final boolean isJspFileWithStrutsSupport = JspPsiUtil.getJspFile(holder.getFile()) != null && StrutsFacet.getInstance(holder.getFile()) != null; @Nullable final String actionExtension; if (isJspFileWithStrutsSupport) { actionExtension = ContainerUtil.getFirstItem(StrutsConstantHelper.getActionExtensions(holder.getFile())); } else { actionExtension = null; } return new XmlElementVisitor() { @Override public void visitXmlAttributeValue(XmlAttributeValue value) { if (!isJspFileWithStrutsSupport || actionExtension == null) { return; } XmlTag tag = PsiTreeUtil.getParentOfType(value, XmlTag.class); if (tag == null) return; URL parsedURL = parseURL(value, actionExtension); if (parsedURL == null) return; if (buildTag("", parsedURL, "", false, actionExtension) == null) return; TextRange range = ElementManipulators.getValueTextRange(value); holder.registerProblem(value, range, "Use Struts <url> tag instead of hardcoded URL", new WrapWithSUrl(actionExtension)); } }; } @NotNull @Override public String[] getGroupPath() { return new String[]{StrutsBundle.message("inspections.group.path.name"), getGroupDisplayName()}; } private static class WrapWithSUrl implements LocalQuickFix { private final String myActionExtension; private WrapWithSUrl(String actionExtension) { myActionExtension = actionExtension; } @SuppressWarnings("DialogTitleCapitalization") @NotNull @Override public String getFamilyName() { return "Wrap with Struts <url> tag"; } @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { PsiElement element = descriptor.getPsiElement(); if (element instanceof XmlAttributeValue) { final XmlAttributeValue value = (XmlAttributeValue)element; XmlTag tag = PsiTreeUtil.getParentOfType(value, XmlTag.class, false); final boolean inline = tag instanceof HtmlTag; final URL url = parseURL(value, myActionExtension); if (url == null) { return; } final JspFile jspFile = JspPsiUtil.getJspFile(value); assert jspFile != null; XmlTag rootTag = jspFile.getRootTag(); String prefix = rootTag.getPrefixByNamespace(StrutsConstants.TAGLIB_STRUTS_UI_URI); if (StringUtil.isEmpty(prefix)) { XmlNamespaceHelper extension = XmlNamespaceHelper.getHelper(jspFile); prefix = ExtendedTagInsertHandler.suggestPrefix(jspFile, StrutsConstants.TAGLIB_STRUTS_UI_URI); XmlNamespaceHelper.Runner<String, IncorrectOperationException> after = new XmlNamespaceHelper.Runner<String, IncorrectOperationException>() { @Override public void run(String param) throws IncorrectOperationException { wrapValue(param, value, url, inline); } }; extension.insertNamespaceDeclaration(jspFile, null, Collections.singleton(StrutsConstants.TAGLIB_STRUTS_UI_URI), prefix, after); } else { wrapValue(prefix, value, url, inline); } } } private void wrapValue(String prefix, XmlAttributeValue value, URL url, boolean inline) { final JspFile jspFile = JspPsiUtil.getJspFile(value); assert jspFile != null; Project project = jspFile.getProject(); TextRange range = value.getValueTextRange(); Document document = PsiDocumentManager.getInstance(project).getDocument(jspFile); assert document != null; PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document); int start = range.getStartOffset(); int lineStart = document.getLineStartOffset(document.getLineNumber(start)); String linePrefix = document.getCharsSequence().subSequence(lineStart, start).toString(); linePrefix = linePrefix.substring(0, linePrefix.length() - linePrefix.trim().length()); String indent = linePrefix; while (indent.length() < start - lineStart) indent += " "; Pair<String, String> tag_var = buildTag(prefix, url, indent, inline, myActionExtension); String tag = tag_var.getFirst(); String var = tag_var.getSecond(); int end = range.getEndOffset(); int formattingStart; int formattingEnd; if (inline) { document.replaceString(start, end, tag); formattingStart = start; formattingEnd = start + tag.length(); } else { document.replaceString(start, end, "${" + var + "}"); XmlTag containingTag = PsiTreeUtil.getParentOfType(value, XmlTag.class, false); assert containingTag != null; int startOffset = containingTag.getTextRange().getStartOffset(); document.insertString(startOffset, "\n"); document.insertString(startOffset, tag); formattingStart = startOffset; formattingEnd = startOffset + tag.length() + 2; } PsiDocumentManager.getInstance(project).commitDocument(document); CodeStyleManager.getInstance(project).reformatText(jspFile, formattingStart, formattingEnd); } } private static Pair<String, String> buildTag(String prefix, URL url, String indent, boolean inline, String actionExtension) { String path = url.getPath(); int slash = path.lastIndexOf('/'); String namespace = slash > 0 ? path.substring(0, slash) : null; String action = slash != -1 ? path.substring(slash + 1) : path; action = StringUtil.trimEnd(action, actionExtension); int exclamationIdx = action.indexOf('!'); String method = null; if (exclamationIdx > 0) { method = action.substring(exclamationIdx + 1); action = action.substring(0, exclamationIdx); } StringBuilder sb = new StringBuilder(); sb.append('<').append(prefix).append(":url"); String var; if (inline) { var = null; } else { var = action + "_url"; sb.append(" var=\"").append(var).append("\""); } if (namespace != null) { sb.append(" namespace=\"").append(namespace).append("\""); } sb.append(" action=\"").append(action).append("\""); if (method != null) { sb.append(" method=\"").append(method).append("\""); } String query = url.getQuery(); if (StringUtil.isEmpty(query)) { sb.append("/>"); } else { sb.append(">"); for (String escapedArg : StringUtil.split(query, "&")) { for (String arg : StringUtil.split(escapedArg, "&")) { int eq = arg.indexOf('='); String name = eq > 0 ? arg.substring(0, eq) : arg; String value = eq > 0 ? arg.substring(eq + 1) : ""; if (name.contains("[") || name.contains("$")) return null; // This will not work if arg name is actually an expression sb.append("\n").append(indent).append(" <") .append(prefix) .append(":param name=\"") .append(name).append("\">") .append(value) .append("</") .append(prefix) .append(":param>"); } } sb.append('\n').append(indent); sb.append("</").append(prefix).append(":url>"); } return Pair.create(sb.toString(), var); } @Nullable private static URL parseURL(XmlAttributeValue value, String actionExtension) { String rawUrl = value.getValue(); if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) { return null; } URL parsedURL; try { parsedURL = new URL("http://" + rawUrl); } catch (Exception e) { return null; } String host = parsedURL.getHost(); if (!StringUtil.isEmpty(host) && !(host.startsWith("${") && host.endsWith("}"))) { return null; } String path = parsedURL.getPath(); if (!path.endsWith(actionExtension)) { return null; } if (path.contains("${")) { return null; // Dynamic action paths cannot be converted. } return parsedURL; } }