/** * 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; import com.liferay.portal.kernel.nio.charset.CharsetDecoderUtil; import com.liferay.portal.kernel.util.ArrayUtil; import com.liferay.portal.kernel.util.CharPool; import com.liferay.portal.kernel.util.GetterUtil; import com.liferay.portal.kernel.util.ListUtil; import com.liferay.portal.kernel.util.ReflectionUtil; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.Validator; import com.liferay.portal.tools.ToolsUtil; import com.liferay.source.formatter.checks.FileCheck; import com.liferay.source.formatter.checks.JavaTermCheck; import com.liferay.source.formatter.checks.SourceCheck; import com.liferay.source.formatter.checks.configuration.ConfigurationLoader; import com.liferay.source.formatter.checks.configuration.SourceCheckConfiguration; import com.liferay.source.formatter.checks.configuration.SourceFormatterConfiguration; import com.liferay.source.formatter.checks.util.JavaSourceUtil; import com.liferay.source.formatter.parser.JavaClass; import com.liferay.source.formatter.parser.JavaClassParser; import com.liferay.source.formatter.parser.ParseException; import com.liferay.source.formatter.util.FileUtil; import com.liferay.source.formatter.util.SourceFormatterUtil; import java.awt.Desktop; import java.io.File; import java.lang.reflect.Constructor; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.regex.Pattern; import org.apache.commons.beanutils.BeanUtils; import org.apache.tools.ant.types.selectors.SelectorUtils; /** * @author Brian Wing Shun Chan * @author Igor Spasic * @author Wesley Gong * @author Hugo Huijser */ public abstract class BaseSourceProcessor implements SourceProcessor { public static final int PLUGINS_MAX_DIR_LEVEL = ToolsUtil.PLUGINS_MAX_DIR_LEVEL; public static final int PORTAL_MAX_DIR_LEVEL = ToolsUtil.PORTAL_MAX_DIR_LEVEL; @Override public final void format() throws Exception { if (sourceFormatterArgs.isShowDocumentation()) { System.setProperty("java.awt.headless", "false"); } List<String> fileNames = getFileNames(); if (fileNames.isEmpty()) { return; } _pluginsInsideModulesDirectoryNames = getPluginsInsideModulesDirectoryNames(); _sourceChecks = _getSourceChecks(_containsModuleFile(fileNames)); ExecutorService executorService = Executors.newFixedThreadPool( sourceFormatterArgs.getProcessorThreadCount()); List<Future<Void>> futures = new ArrayList<>(fileNames.size()); for (final String fileName : fileNames) { Future<Void> future = executorService.submit( new Callable<Void>() { @Override public Void call() throws Exception { try { _format(fileName); return null; } catch (Exception e) { throw new RuntimeException( "Unable to format " + fileName, e); } } }); futures.add(future); } for (Future<Void> future : futures) { future.get(); } executorService.shutdown(); postFormat(); } public final List<String> getFileNames() throws Exception { List<String> fileNames = sourceFormatterArgs.getFileNames(); if (fileNames != null) { return fileNames; } return doGetFileNames(); } @Override public SourceMismatchException getFirstSourceMismatchException() { return _firstSourceMismatchException; } @Override public String[] getIncludes() { return filterIncludes(doGetIncludes()); } @Override public List<String> getModifiedFileNames() { return _modifiedFileNames; } @Override public Set<SourceFormatterMessage> getSourceFormatterMessages() { Set<SourceFormatterMessage> sourceFormatterMessages = new TreeSet<>(); for (Map.Entry<String, Set<SourceFormatterMessage>> entry : _sourceFormatterMessagesMap.entrySet()) { sourceFormatterMessages.addAll(entry.getValue()); } return sourceFormatterMessages; } @Override public void setAllFileNames(List<String> allFileNames) { _allFileNames = allFileNames; } @Override public void setProperties(Properties properties) { _properties = properties; } @Override public void setSourceFormatterArgs( SourceFormatterArgs sourceFormatterArgs) { this.sourceFormatterArgs = sourceFormatterArgs; _init(); } protected abstract List<String> doGetFileNames() throws Exception; protected abstract String[] doGetIncludes(); protected String[] filterIncludes(String[] includes) { List<String> fileExtensions = sourceFormatterArgs.getFileExtensions(); if (fileExtensions.isEmpty()) { return includes; } String[] filteredIncludes = new String[0]; for (String include : includes) { for (String fileExtension : fileExtensions) { if (include.endsWith(fileExtension)) { filteredIncludes = ArrayUtil.append( filteredIncludes, include); } } } return filteredIncludes; } protected File getFile(String fileName, int level) { return SourceFormatterUtil.getFile( sourceFormatterArgs.getBaseDirName(), fileName, level); } protected List<String> getFileNames( String basedir, String[] excludes, String[] includes) throws Exception { if (_excludes != null) { excludes = ArrayUtil.append(excludes, _excludes); } return SourceFormatterUtil.scanForFiles( basedir, excludes, includes, sourceFormatterArgs.isIncludeSubrepositories()); } protected List<String> getFileNames(String[] excludes, String[] includes) throws Exception { return getFileNames(excludes, includes, false); } protected List<String> getFileNames( String[] excludes, String[] includes, boolean forceIncludeAllFiles) throws Exception { if (_excludes != null) { excludes = ArrayUtil.append(excludes, _excludes); } if (!forceIncludeAllFiles && (sourceFormatterArgs.getRecentChangesFileNames() != null)) { return SourceFormatterUtil.filterRecentChangesFileNames( sourceFormatterArgs.getBaseDirName(), sourceFormatterArgs.getRecentChangesFileNames(), excludes, includes, sourceFormatterArgs.isIncludeSubrepositories()); } return SourceFormatterUtil.filterFileNames( _allFileNames, excludes, includes); } protected List<String> getPluginsInsideModulesDirectoryNames() throws Exception { if (_pluginsInsideModulesDirectoryNames != null) { return _pluginsInsideModulesDirectoryNames; } List<String> pluginsInsideModulesDirectoryNames = new ArrayList<>(); List<String> pluginBuildFileNames = getFileNames( new String[0], new String[] { "**/modules/apps/**/build.xml", "**/modules/private/apps/**/build.xml" }, true); for (String pluginBuildFileName : pluginBuildFileNames) { pluginBuildFileName = StringUtil.replace( pluginBuildFileName, CharPool.BACK_SLASH, CharPool.SLASH); String absolutePath = JavaSourceUtil.getAbsolutePath( pluginBuildFileName); int x = absolutePath.indexOf("/modules/apps/"); if (x == -1) { x = absolutePath.indexOf("/modules/private/apps/"); } int y = absolutePath.lastIndexOf(StringPool.SLASH); pluginsInsideModulesDirectoryNames.add( absolutePath.substring(x, y + 1)); } return pluginsInsideModulesDirectoryNames; } protected String getProperty(String key) { return _properties.getProperty(key); } protected List<String> getPropertyList(String key) { return ListUtil.fromString( GetterUtil.getString(getProperty(key)), StringPool.COMMA); } protected void postFormat() throws Exception { } protected void printError(String fileName, String message) { if (sourceFormatterArgs.isPrintErrors()) { SourceFormatterUtil.printError(fileName, message); } } protected void processFormattedFile( File file, String fileName, String content, String newContent) throws Exception { if (!content.equals(newContent)) { if (sourceFormatterArgs.isPrintErrors()) { SourceFormatterUtil.printError(fileName, file); } if (sourceFormatterArgs.isAutoFix()) { FileUtil.write(file, newContent); } else if (_firstSourceMismatchException == null) { _firstSourceMismatchException = new SourceMismatchException( fileName, content, newContent); } } if (sourceFormatterArgs.isPrintErrors()) { Set<SourceFormatterMessage> sourceFormatterMessages = _sourceFormatterMessagesMap.get(fileName); if (sourceFormatterMessages != null) { for (SourceFormatterMessage sourceFormatterMessage : sourceFormatterMessages) { SourceFormatterUtil.printError( fileName, sourceFormatterMessage.toString()); if (_browserStarted || !sourceFormatterArgs.isShowDocumentation() || !Desktop.isDesktopSupported()) { continue; } String markdownFileName = sourceFormatterMessage.getMarkdownFileName(); if (Validator.isNotNull(markdownFileName)) { Desktop desktop = Desktop.getDesktop(); desktop.browse( new URI(_DOCUMENTATION_URL + markdownFileName)); _browserStarted = true; } } } } _modifiedFileNames.add(file.getAbsolutePath()); } protected void processMessage( String fileName, SourceFormatterMessage sourceFormatterMessage) { Set<SourceFormatterMessage> sourceFormatterMessages = _sourceFormatterMessagesMap.get(fileName); if (sourceFormatterMessages == null) { sourceFormatterMessages = new TreeSet<>(); } sourceFormatterMessages.add(sourceFormatterMessage); _sourceFormatterMessagesMap.put(fileName, sourceFormatterMessages); } protected void processMessage(String fileName, String message) { processMessage( fileName, new SourceFormatterMessage(fileName, message, null, -1)); } protected String processSourceChecks( File file, String fileName, String absolutePath, String content) throws Exception { if (ListUtil.isEmpty(_sourceChecks)) { return content; } JavaClass javaClass = null; List<JavaClass> anonymousClasses = null; for (SourceCheck sourceCheck : _sourceChecks) { if (sourceCheck.isModulesCheck() && !_isModulesFile(absolutePath)) { continue; } String newContent = null; if (sourceCheck instanceof FileCheck) { newContent = _processFileCheck( (FileCheck)sourceCheck, fileName, absolutePath, content); } else if ((sourceCheck instanceof JavaTermCheck) && (this instanceof JavaSourceProcessor)) { if (javaClass == null) { try { anonymousClasses = JavaClassParser.parseAnonymousClasses(content); javaClass = JavaClassParser.parseJavaClass( fileName, content); } catch (ParseException pe) { processMessage(fileName, pe.getMessage()); continue; } } newContent = _processJavaTermCheck( (JavaTermCheck)sourceCheck, javaClass, anonymousClasses, fileName, absolutePath, content); } if (!newContent.equals(content)) { return newContent; } } return content; } protected static Pattern javaSourceInsideJSPLinePattern = Pattern.compile( "<%=(.+?)%>"); protected static boolean portalSource; protected static boolean subrepository; protected SourceFormatterArgs sourceFormatterArgs; private void _checkUTF8(File file, String fileName) throws Exception { byte[] bytes = FileUtil.getBytes(file); try { CharsetDecoder charsetDecoder = CharsetDecoderUtil.getCharsetDecoder( StringPool.UTF8, CodingErrorAction.REPORT); charsetDecoder.decode(ByteBuffer.wrap(bytes)); } catch (Exception e) { processMessage(fileName, "UTF-8"); } } private boolean _containsModuleFile(List<String> fileNames) { for (String fileName : fileNames) { if (!_isMatchPath(fileName)) { continue; } String absolutePath = JavaSourceUtil.getAbsolutePath(fileName); if (_isModulesFile(absolutePath, true)) { return true; } } return false; } private final String _format( File file, String fileName, String absolutePath, String content) throws Exception { _sourceFormatterMessagesMap.remove(fileName); _checkUTF8(file, fileName); String newContent = processSourceChecks( file, fileName, absolutePath, content); if (content.equals(newContent)) { return content; } return _format(file, fileName, absolutePath, newContent); } private final void _format(String fileName) throws Exception { if (!_isMatchPath(fileName)) { return; } fileName = StringUtil.replace( fileName, CharPool.BACK_SLASH, CharPool.SLASH); String absolutePath = JavaSourceUtil.getAbsolutePath(fileName); File file = new File(absolutePath); String content = FileUtil.read(file); String newContent = _format(file, fileName, absolutePath, content); processFormattedFile(file, fileName, content, newContent); } private String[] _getExcludes() { if (sourceFormatterArgs.getFileNames() != null) { return new String[0]; } List<String> excludesList = ListUtil.fromString( GetterUtil.getString( System.getProperty("source.formatter.excludes"))); excludesList.addAll(getPropertyList("source.formatter.excludes")); return excludesList.toArray(new String[excludesList.size()]); } private List<SourceCheck> _getSourceChecks(boolean includeModuleChecks) throws Exception { SourceFormatterConfiguration sourceFormatterConfiguration = ConfigurationLoader.loadConfiguration("sourcechecks.xml"); Class<?> clazz = getClass(); List<SourceCheck> sourceChecks = _getSourceChecks( sourceFormatterConfiguration, clazz.getSimpleName(), includeModuleChecks); sourceChecks.addAll( _getSourceChecks( sourceFormatterConfiguration, "all", includeModuleChecks)); return sourceChecks; } private List<SourceCheck> _getSourceChecks( SourceFormatterConfiguration sourceFormatterConfiguration, String sourceProcessorName, boolean includeModuleChecks) throws Exception { List<SourceCheck> sourceChecks = new ArrayList<>(); List<SourceCheckConfiguration> sourceCheckConfigurations = sourceFormatterConfiguration.getSourceCheckConfigurations( sourceProcessorName); if (sourceCheckConfigurations == null) { return sourceChecks; } for (SourceCheckConfiguration sourceCheckConfiguration : sourceCheckConfigurations) { String sourceCheckName = sourceCheckConfiguration.getName(); if (!sourceCheckName.contains(StringPool.PERIOD)) { sourceCheckName = "com.liferay.source.formatter.checks." + sourceCheckName; } Class<?> sourceCheckClass = null; try { sourceCheckClass = Class.forName(sourceCheckName); } catch (ClassNotFoundException cnfe) { SourceFormatterUtil.printError( "sourcechecks.xml", "sourcechecks.xml: Class " + sourceCheckName + " cannot be found"); continue; } Constructor<?> declaredConstructor = sourceCheckClass.getDeclaredConstructor(); Object instance = declaredConstructor.newInstance(); if (!(instance instanceof SourceCheck)) { continue; } SourceCheck sourceCheck = (SourceCheck)instance; if ((!portalSource && !subrepository && sourceCheck.isPortalCheck()) || (!includeModuleChecks && sourceCheck.isModulesCheck())) { continue; } for (String attributeName : sourceCheckConfiguration.attributeNames()) { BeanUtils.setProperty( sourceCheck, attributeName, sourceCheckConfiguration.getAttributeValue(attributeName)); } _initSourceCheck(sourceCheck); sourceChecks.add(sourceCheck); } return sourceChecks; } private void _init() { try { portalSource = _isPortalSource(); subrepository = _isSubrepository(); _sourceFormatterMessagesMap = new HashMap<>(); } catch (Exception e) { ReflectionUtil.throwException(e); } _excludes = _getExcludes(); } private void _initSourceCheck(SourceCheck sourceCheck) throws Exception { sourceCheck.setAllFileNames(_allFileNames); sourceCheck.setBaseDirName(sourceFormatterArgs.getBaseDirName()); sourceCheck.setExcludes(_excludes); sourceCheck.setMaxLineLength(sourceFormatterArgs.getMaxLineLength()); sourceCheck.setPluginsInsideModulesDirectoryNames( _pluginsInsideModulesDirectoryNames); sourceCheck.setPortalSource(portalSource); sourceCheck.setProperties(_properties); sourceCheck.setSubrepository(subrepository); sourceCheck.init(); } private boolean _isMatchPath(String fileName) { for (String pattern : getIncludes()) { if (SelectorUtils.matchPath(_normalizePattern(pattern), fileName)) { return true; } } return false; } private boolean _isModulesFile(String absolutePath) { return _isModulesFile(absolutePath, false); } private boolean _isModulesFile( String absolutePath, boolean includePlugins) { if (subrepository) { return true; } if (includePlugins) { return absolutePath.contains("/modules/"); } try { for (String directoryName : getPluginsInsideModulesDirectoryNames()) { if (absolutePath.contains(directoryName)) { return false; } } } catch (Exception e) { } return absolutePath.contains("/modules/"); } private boolean _isPortalSource() { if (getFile("portal-impl", PORTAL_MAX_DIR_LEVEL) != null) { return true; } return false; } private boolean _isSubrepository() { String baseDirAbsolutePath = JavaSourceUtil.getAbsolutePath( sourceFormatterArgs.getBaseDirName()); int x = baseDirAbsolutePath.length(); for (int i = 0; i < _SUBREPOSITORY_MAX_DIR_LEVEL; i++) { x = baseDirAbsolutePath.lastIndexOf(CharPool.FORWARD_SLASH, x - 1); if (x == -1) { return false; } String dirName = baseDirAbsolutePath.substring(x + 1); if (dirName.startsWith("com-liferay-")) { return true; } } return false; } private String _normalizePattern(String originalPattern) { String pattern = originalPattern.replace( CharPool.SLASH, File.separatorChar); pattern = pattern.replace(CharPool.BACK_SLASH, File.separatorChar); if (pattern.endsWith(File.separator)) { pattern += SelectorUtils.DEEP_TREE_MATCH; } return pattern; } private String _processFileCheck( FileCheck fileCheck, String fileName, String absolutePath, String content) throws Exception { content = fileCheck.process(fileName, absolutePath, content); for (SourceFormatterMessage sourceFormatterMessage : fileCheck.getSourceFormatterMessages(fileName)) { processMessage(fileName, sourceFormatterMessage); } return content; } private String _processJavaTermCheck( JavaTermCheck javaTermCheck, JavaClass javaClass, List<JavaClass> anonymousClasses, String fileName, String absolutePath, String content) throws Exception { content = javaTermCheck.process( fileName, absolutePath, javaClass, content); for (SourceFormatterMessage sourceFormatterMessage : javaTermCheck.getSourceFormatterMessages(fileName)) { processMessage(fileName, sourceFormatterMessage); } for (JavaClass anonymousClass : anonymousClasses) { content = javaTermCheck.process( fileName, absolutePath, anonymousClass, content); for (SourceFormatterMessage sourceFormatterMessage : javaTermCheck.getSourceFormatterMessages(fileName)) { processMessage(fileName, sourceFormatterMessage); } } return content; } private static final String _DOCUMENTATION_URL = "https://github.com/liferay/liferay-portal/blob/master/modules/util" + "/source-formatter/documentation/"; private static final int _SUBREPOSITORY_MAX_DIR_LEVEL = 3; private List<String> _allFileNames; private boolean _browserStarted; private String[] _excludes; private SourceMismatchException _firstSourceMismatchException; private final List<String> _modifiedFileNames = new CopyOnWriteArrayList<>(); private List<String> _pluginsInsideModulesDirectoryNames; private Properties _properties; private List<SourceCheck> _sourceChecks = new ArrayList<>(); private Map<String, Set<SourceFormatterMessage>> _sourceFormatterMessagesMap = new ConcurrentHashMap<>(); }