/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.source.formatter.checks;
import com.liferay.portal.kernel.io.unsync.UnsyncBufferedReader;
import com.liferay.portal.kernel.io.unsync.UnsyncStringReader;
import com.liferay.portal.kernel.util.ArrayUtil;
import com.liferay.portal.kernel.util.CharPool;
import com.liferay.portal.kernel.util.ListUtil;
import com.liferay.portal.kernel.util.SetUtil;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.TextFormatter;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.tools.ToolsUtil;
import com.liferay.source.formatter.checks.util.SourceUtil;
import com.liferay.source.formatter.util.FileUtil;
import com.liferay.source.formatter.util.SourceFormatterUtil;
import com.liferay.source.formatter.util.ThreadSafeClassLibrary;
import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.model.DefaultDocletTagFactory;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.Type;
import com.thoughtworks.qdox.parser.ParseException;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.dom4j.Document;
import org.dom4j.Element;
/**
* @author Hugo Huijser
*/
public class JSPTagAttributesCheck extends TagAttributesCheck {
@Override
public void init() throws Exception {
_primitiveTagAttributeDataTypes = _getPrimitiveTagAttributeDataTypes();
_tagJavaClassesMap = _getTagJavaClassesMap();
}
@Override
public void setAllFileNames(List<String> allFileNames) {
_allFileNames = allFileNames;
}
@Override
protected String doProcess(
String fileName, String absolutePath, String content)
throws Exception {
content = _formatSingleLineTagAttribues(fileName, content);
content = _formatMultiLinesTagAttribues(fileName, content);
return content;
}
@Override
protected String formatTagAttributeType(
String line, String tagName, String attributeAndValue)
throws Exception {
if (attributeAndValue.matches(
".*=\"<%= Boolean\\.(FALSE|TRUE) %>\".*")) {
String newAttributeAndValue = StringUtil.replace(
attributeAndValue,
new String[] {
"=\"<%= Boolean.FALSE %>\"", "=\"<%= Boolean.TRUE %>\""
},
new String[] {"=\"<%= false %>\"", "=\"<%= true %>\""});
return StringUtil.replace(
line, attributeAndValue, newAttributeAndValue);
}
if (!isPortalSource() && !isSubrepository()) {
return line;
}
if (!attributeAndValue.endsWith(StringPool.QUOTE) ||
attributeAndValue.contains("\"<%=")) {
return line;
}
JavaClass tagJavaClass = _tagJavaClassesMap.get(tagName);
if (tagJavaClass == null) {
return line;
}
int pos = attributeAndValue.indexOf("=\"");
String attribute = attributeAndValue.substring(0, pos);
String setAttributeMethodName =
"set" + TextFormatter.format(attribute, TextFormatter.G);
for (String dataType : _primitiveTagAttributeDataTypes) {
Type javaType = new Type(dataType);
JavaMethod setAttributeMethod = null;
while (true) {
// com.thoughtworks.qdox.model.JavaClass is not thread-safe and
// can throw NPE as a result of a race condition
try {
setAttributeMethod = tagJavaClass.getMethodBySignature(
setAttributeMethodName, new Type[] {javaType}, true);
break;
}
catch (Exception e) {
}
}
if (setAttributeMethod != null) {
String value = attributeAndValue.substring(
pos + 2, attributeAndValue.length() - 1);
if (!_isValidTagAttributeValue(value, dataType)) {
return line;
}
String newAttributeAndValue = StringUtil.replace(
attributeAndValue,
StringPool.QUOTE + value + StringPool.QUOTE,
"\"<%= " + value + " %>\"");
return StringUtil.replace(
line, attributeAndValue, newAttributeAndValue);
}
}
if (!attributeAndValue.matches(".*=\"(false|true)\".*")) {
return line;
}
JavaMethod setAttributeMethod = tagJavaClass.getMethodBySignature(
setAttributeMethodName, new Type[] {new Type("java.lang.String")},
true);
if (setAttributeMethod == null) {
return line;
}
String newAttributeAndValue = StringUtil.replace(
attributeAndValue, new String[] {"=\"false\"", "=\"true\""},
new String[] {
"=\"<%= Boolean.FALSE.toString() %>\"",
"=\"<%= Boolean.TRUE.toString() %>\""
});
return StringUtil.replace(
line, attributeAndValue, newAttributeAndValue);
}
@Override
protected String sortHTMLTagAttributes(
String line, String value, String attributeAndValue) {
if (!value.matches("([-a-z0-9]+ )+[-a-z0-9]+")) {
return line;
}
List<String> htmlAttributes = ListUtil.fromArray(
StringUtil.split(value, StringPool.SPACE));
Collections.sort(htmlAttributes);
String newValue = StringUtil.merge(htmlAttributes, StringPool.SPACE);
if (value.equals(newValue)) {
return line;
}
String newAttributeAndValue = StringUtil.replace(
attributeAndValue, value, newValue);
return StringUtil.replace(
line, attributeAndValue, newAttributeAndValue);
}
private String _formatMultiLinesTagAttribues(
String fileName, String content)
throws Exception {
Matcher matcher = _multilineTagPattern.matcher(content);
while (matcher.find()) {
char beforeClosingTagChar = content.charAt(matcher.start(2) - 1);
if ((beforeClosingTagChar != CharPool.NEW_LINE) &&
(beforeClosingTagChar != CharPool.TAB)) {
String closingTag = matcher.group(2);
String whitespace = matcher.group(1);
String tabs = StringUtil.removeChar(
whitespace, CharPool.NEW_LINE);
return StringUtil.replaceFirst(
content, closingTag, "\n" + tabs + closingTag,
matcher.start(2));
}
String tag = matcher.group();
String singlelineTag = StringUtil.removeChar(
StringUtil.trim(tag), CharPool.TAB);
singlelineTag = StringUtil.replace(
singlelineTag, CharPool.NEW_LINE, CharPool.SPACE);
String newTag = formatTagAttributes(
fileName, tag, singlelineTag,
getLineCount(content, matcher.end(1)), false);
if (!tag.equals(newTag)) {
return StringUtil.replace(content, tag, newTag);
}
}
return content;
}
private String _formatSingleLineTagAttribues(
String fileName, String content)
throws Exception {
StringBundler sb = new StringBundler();
try (UnsyncBufferedReader unsyncBufferedReader =
new UnsyncBufferedReader(new UnsyncStringReader(content))) {
int lineCount = 0;
String line = null;
while ((line = unsyncBufferedReader.readLine()) != null) {
lineCount++;
String trimmedLine = StringUtil.trimLeading(line);
if (trimmedLine.matches("<\\w+ .*>.*")) {
line = formatTagAttributes(
fileName, line, trimmedLine, lineCount, false);
}
Matcher matcher = _jspTaglibPattern.matcher(line);
while (matcher.find()) {
line = formatTagAttributes(
fileName, line, line.substring(matcher.start()),
lineCount, false);
}
sb.append(line);
sb.append("\n");
}
}
content = sb.toString();
if (content.endsWith("\n")) {
content = content.substring(0, content.length() - 1);
}
return content;
}
private Set<String> _getPrimitiveTagAttributeDataTypes() {
return SetUtil.fromArray(
new String[] {
"java.lang.Boolean", "Boolean", "boolean", "java.lang.Double",
"Double", "double", "java.lang.Integer", "Integer", "int",
"java.lang.Long", "Long", "long"
});
}
private Map<String, JavaClass> _getTagJavaClassesMap() throws Exception {
Map<String, JavaClass> tagJavaClassesMap = new HashMap<>();
outerLoop:
for (String tldFileName : _getTLDFileNames()) {
tldFileName = StringUtil.replace(
tldFileName, CharPool.BACK_SLASH, CharPool.SLASH);
File tldFile = new File(tldFileName);
String content = FileUtil.read(tldFile);
Document document = SourceUtil.readXML(content);
Element rootElement = document.getRootElement();
Element shortNameElement = rootElement.element("short-name");
String shortName = shortNameElement.getStringValue();
List<Element> tagElements = rootElement.elements("tag");
String srcDir = null;
for (Element tagElement : tagElements) {
Element tagClassElement = tagElement.element("tag-class");
String tagClassName = tagClassElement.getStringValue();
if (!tagClassName.startsWith("com.liferay")) {
continue;
}
if (srcDir == null) {
if (tldFileName.contains("/src/")) {
srcDir = tldFile.getAbsolutePath();
srcDir = StringUtil.replace(
srcDir, CharPool.BACK_SLASH, CharPool.SLASH);
srcDir =
srcDir.substring(0, srcDir.lastIndexOf("/src/")) +
"/src/main/java/";
}
else {
srcDir = _getUtilTaglibSrcDirName();
if (Validator.isNull(srcDir)) {
continue outerLoop;
}
}
}
StringBundler sb = new StringBundler(3);
sb.append(srcDir);
sb.append(
StringUtil.replace(
tagClassName, CharPool.PERIOD, CharPool.SLASH));
sb.append(".java");
File tagJavaFile = new File(sb.toString());
if (!tagJavaFile.exists()) {
continue;
}
JavaDocBuilder javaDocBuilder = new JavaDocBuilder(
new DefaultDocletTagFactory(),
new ThreadSafeClassLibrary());
try {
javaDocBuilder.addSource(tagJavaFile);
}
catch (ParseException pe) {
continue;
}
JavaClass tagJavaClass = javaDocBuilder.getClassByName(
tagClassName);
Element tagNameElement = tagElement.element("name");
String tagName = tagNameElement.getStringValue();
tagJavaClassesMap.put(
shortName + StringPool.COLON + tagName, tagJavaClass);
}
}
return tagJavaClassesMap;
}
private List<String> _getTLDFileNames() throws Exception {
String[] excludes = new String[] {
"**/dependencies/**", "**/util-taglib/**", "**/portal-web/**"
};
if (getExcludes() != null) {
excludes = ArrayUtil.append(excludes, getExcludes());
}
List<String> tldFileNames = SourceFormatterUtil.filterFileNames(
_allFileNames, excludes, new String[] {"**/*.tld"});
if (!isPortalSource()) {
return tldFileNames;
}
String tldDirLocation = "portal-web/docroot/WEB-INF/tld/";
for (int i = 0; i < ToolsUtil.PORTAL_MAX_DIR_LEVEL - 1; i++) {
File file = new File(getBaseDirName() + tldDirLocation);
if (file.exists()) {
tldFileNames.addAll(
getFileNames(
getBaseDirName() + tldDirLocation, new String[0],
new String[] {"**/*.tld"}));
break;
}
tldDirLocation = "../" + tldDirLocation;
}
return tldFileNames;
}
private String _getUtilTaglibSrcDirName() {
File utilTaglibDir = getFile(
"util-taglib/src", ToolsUtil.PORTAL_MAX_DIR_LEVEL);
if (utilTaglibDir == null) {
return StringPool.BLANK;
}
String utilTaglibSrcDirName = utilTaglibDir.getAbsolutePath();
utilTaglibSrcDirName = StringUtil.replace(
utilTaglibSrcDirName, CharPool.BACK_SLASH, CharPool.SLASH);
utilTaglibSrcDirName += StringPool.SLASH;
return utilTaglibSrcDirName;
}
private boolean _isValidTagAttributeValue(String value, String dataType) {
if (dataType.endsWith("Boolean") || dataType.equals("boolean")) {
return Validator.isBoolean(value);
}
if (dataType.endsWith("Double") || dataType.equals("double")) {
try {
Double.parseDouble(value);
}
catch (NumberFormatException nfe) {
return false;
}
return true;
}
if (dataType.endsWith("Integer") || dataType.equals("int") ||
dataType.endsWith("Long") || dataType.equals("long")) {
return Validator.isNumber(value);
}
return false;
}
private List<String> _allFileNames;
private final Pattern _jspTaglibPattern = Pattern.compile(
"<[-\\w]+:[-\\w]+ .");
private final Pattern _multilineTagPattern = Pattern.compile(
"(\\s+)<[-\\w]+:[-\\w]+\n.*?(/?>)(\n|$)", Pattern.DOTALL);
private Set<String> _primitiveTagAttributeDataTypes;
private Map<String, JavaClass> _tagJavaClassesMap;
}