/**
* 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.css.builder;
import com.liferay.portal.kernel.regex.PatternFactory;
import com.liferay.portal.kernel.util.GetterUtil;
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.ArgumentsUtil;
import com.liferay.rtl.css.RTLCSSConverter;
import com.liferay.sass.compiler.SassCompiler;
import com.liferay.sass.compiler.SassCompilerException;
import com.liferay.sass.compiler.jni.internal.JniSassCompiler;
import com.liferay.sass.compiler.ruby.internal.RubySassCompiler;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.tools.ant.DirectoryScanner;
/**
* @author Brian Wing Shun Chan
* @author Raymond Augé
* @author Eduardo Lundgren
* @author Shuyang Zhou
* @author David Truong
*/
public class CSSBuilder implements AutoCloseable {
public static void main(String[] args) throws Exception {
Map<String, String> arguments = ArgumentsUtil.parseArguments(args);
List<String> dirNames = new ArrayList<>();
String dirName = GetterUtil.getString(
arguments.get("sass.dir"), CSSBuilderArgs.DIR_NAME);
if (Validator.isNotNull(dirName)) {
dirNames.add(dirName);
}
else {
for (int i = 0;; i++) {
dirName = arguments.get("sass.dir." + i);
if (Validator.isNotNull(dirName)) {
dirNames.add(dirName);
}
else {
break;
}
}
}
String docrootDirName = GetterUtil.getString(
arguments.get("sass.docroot.dir"), CSSBuilderArgs.DOCROOT_DIR_NAME);
boolean generateSourceMap = GetterUtil.getBoolean(
arguments.get("sass.generate.source.map"));
String outputDirName = GetterUtil.getString(
arguments.get("sass.output.dir"), CSSBuilderArgs.OUTPUT_DIR_NAME);
String portalCommonPath = arguments.get("sass.portal.common.path");
if (Validator.isNull(portalCommonPath)) {
portalCommonPath = arguments.get("sass.portal.common.dir");
}
int precision = GetterUtil.getInteger(
arguments.get("sass.precision"), CSSBuilderArgs.PRECISION);
String[] rtlExcludedPathRegexps = StringUtil.split(
arguments.get("sass.rtl.excluded.path.regexps"));
String sassCompilerClassName = arguments.get(
"sass.compiler.class.name");
try (CSSBuilder cssBuilder = new CSSBuilder(
docrootDirName, generateSourceMap, outputDirName,
portalCommonPath, precision, rtlExcludedPathRegexps,
sassCompilerClassName)) {
cssBuilder.execute(dirNames);
}
catch (Exception e) {
ArgumentsUtil.processMainException(arguments, e);
}
}
public CSSBuilder(
String docrootDirName, boolean generateSourceMap,
String outputDirName, String portalCommonPath, int precision,
String[] rtlExcludedPathRegexps, String sassCompilerClassName)
throws Exception {
File portalCommonDir = new File(portalCommonPath);
if (portalCommonDir.isFile()) {
portalCommonDir = _unzipPortalCommon(portalCommonDir);
_cleanPortalCommonDir = true;
}
else {
_cleanPortalCommonDir = false;
}
_docrootDirName = docrootDirName;
_generateSourceMap = generateSourceMap;
_outputDirName = outputDirName;
_portalCommonDirName = portalCommonDir.getCanonicalPath();
_precision = precision;
_rtlExcludedPathPatterns = PatternFactory.compile(
rtlExcludedPathRegexps);
_initSassCompiler(sassCompilerClassName);
}
@Override
public void close() throws Exception {
if (_cleanPortalCommonDir) {
_deltree(_portalCommonDirName);
}
}
public void execute(List<String> dirNames) throws Exception {
List<String> fileNames = new ArrayList<>();
for (String dirName : dirNames) {
_collectSassFiles(fileNames, dirName, _docrootDirName);
}
for (String fileName : fileNames) {
long startTime = System.currentTimeMillis();
_parseSassFile(fileName);
System.out.println(
"Parsed " + fileName + " in " +
(System.currentTimeMillis() - startTime) + "ms");
}
}
public boolean isRtlExcludedPath(String filePath) {
for (Pattern pattern : _rtlExcludedPathPatterns) {
Matcher matcher = pattern.matcher(filePath);
if (matcher.matches()) {
return true;
}
}
return false;
}
private void _collectSassFiles(
List<String> fileNames, String dirName, String docrootDirName)
throws Exception {
DirectoryScanner directoryScanner = new DirectoryScanner();
String basedir = docrootDirName.concat(dirName);
directoryScanner.setBasedir(basedir);
directoryScanner.setExcludes(
new String[] {
"**\\_*.scss", "**\\_diffs\\**", "**\\.sass-cache*\\**",
"**\\.sass_cache_*\\**", "**\\_sass_cache_*\\**",
"**\\_styled\\**", "**\\_unstyled\\**", "**\\css\\aui\\**",
"**\\tmp\\**"
});
directoryScanner.setIncludes(new String[] {"**\\*.scss"});
directoryScanner.scan();
String[] fileNamesArray = directoryScanner.getIncludedFiles();
if (!_isModified(basedir, fileNamesArray)) {
return;
}
for (String fileName : fileNamesArray) {
if (fileName.contains("_rtl")) {
continue;
}
fileNames.add(_normalizeFileName(dirName, fileName));
}
}
private void _deltree(String dirName) throws IOException {
Files.walkFileTree(
Paths.get(dirName),
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult postVisitDirectory(
Path dirPath, IOException ioe)
throws IOException {
Files.delete(dirPath);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(
Path path, BasicFileAttributes basicFileAttributes)
throws IOException {
Files.delete(path);
return FileVisitResult.CONTINUE;
}
});
}
private String _getRtlCss(String fileName, String css) throws Exception {
String rtlCss = css;
try {
if (_rtlCSSConverter == null) {
_rtlCSSConverter = new RTLCSSConverter();
}
rtlCss = _rtlCSSConverter.process(rtlCss);
}
catch (Exception e) {
System.out.println(
"Unable to generate RTL version for " + fileName +
StringPool.COMMA_AND_SPACE + e.getMessage());
}
return rtlCss;
}
private void _initSassCompiler(String sassCompilerClassName)
throws Exception {
if (Validator.isNull(sassCompilerClassName) ||
sassCompilerClassName.equals("jni")) {
try {
System.setProperty("jna.nosys", Boolean.TRUE.toString());
_sassCompiler = new JniSassCompiler(_precision);
System.out.println("Using native Sass compiler");
}
catch (Throwable t) {
System.out.println(
"Unable to load native compiler, falling back to Ruby");
_sassCompiler = new RubySassCompiler(_precision);
}
}
else {
try {
_sassCompiler = new RubySassCompiler(_precision);
System.out.println("Using Ruby Sass compiler");
}
catch (Exception e) {
System.out.println(
"Unable to load Ruby compiler, falling back to native");
System.setProperty("jna.nosys", Boolean.TRUE.toString());
_sassCompiler = new JniSassCompiler(_precision);
}
}
}
private boolean _isModified(String dirName, String[] fileNames)
throws Exception {
for (String fileName : fileNames) {
if (fileName.contains("_rtl")) {
continue;
}
fileName = _normalizeFileName(dirName, fileName);
File file = new File(fileName);
File cacheFile = CSSBuilderUtil.getOutputFile(
fileName, _outputDirName);
if (file.lastModified() != cacheFile.lastModified()) {
return true;
}
}
return false;
}
private String _normalizeFileName(String dirName, String fileName) {
fileName = StringUtil.replace(
dirName + StringPool.SLASH + fileName,
new String[] {StringPool.BACK_SLASH, StringPool.DOUBLE_SLASH},
new String[] {StringPool.SLASH, StringPool.SLASH});
return fileName;
}
private String _parseSass(String fileName) throws SassCompilerException {
String filePath = _docrootDirName.concat(fileName);
String cssBasePath = filePath;
int pos = filePath.lastIndexOf("/css/");
if (pos >= 0) {
cssBasePath = filePath.substring(0, pos + 4);
}
else {
pos = filePath.lastIndexOf("/resources/");
if (pos >= 0) {
cssBasePath = filePath.substring(0, pos + 10);
}
}
String css = _sassCompiler.compileFile(
filePath, _portalCommonDirName + File.pathSeparator + cssBasePath,
_generateSourceMap, filePath + ".map");
return CSSBuilderUtil.parseStaticTokens(css);
}
private void _parseSassFile(String fileName) throws Exception {
File file = new File(_docrootDirName, fileName);
if (!file.exists()) {
return;
}
String ltrContent = _parseSass(fileName);
_writeOutputFile(fileName, ltrContent, false);
if (isRtlExcludedPath(fileName)) {
return;
}
String rtlContent = _getRtlCss(fileName, ltrContent);
String rtlCustomFileName = CSSBuilderUtil.getRtlCustomFileName(
fileName);
File rtlCustomFile = new File(_docrootDirName, rtlCustomFileName);
if (rtlCustomFile.exists()) {
rtlContent += _parseSass(rtlCustomFileName);
}
_writeOutputFile(fileName, rtlContent, true);
}
private File _unzipPortalCommon(File portalCommonFile) throws IOException {
Path portalCommonCssDirPath = Files.createTempDirectory(
"portalCommonCss");
try (ZipFile zipFile = new ZipFile(portalCommonFile)) {
Enumeration<? extends ZipEntry> enumeration = zipFile.entries();
while (enumeration.hasMoreElements()) {
ZipEntry zipEntry = enumeration.nextElement();
String name = zipEntry.getName();
if (name.endsWith("/") ||
!name.startsWith("META-INF/resources/")) {
continue;
}
name = name.substring(19);
Path path = portalCommonCssDirPath.resolve(name);
Files.createDirectories(path.getParent());
Files.copy(
zipFile.getInputStream(zipEntry), path,
StandardCopyOption.REPLACE_EXISTING);
}
}
return portalCommonCssDirPath.toFile();
}
private void _write(File file, String content) throws Exception {
File parentFile = file.getParentFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
Path path = Paths.get(file.toURI());
Files.write(path, content.getBytes(StringPool.UTF8));
}
private void _writeOutputFile(String fileName, String content, boolean rtl)
throws Exception {
String outputFileName;
if (rtl) {
String rtlFileName = CSSBuilderUtil.getRtlCustomFileName(fileName);
outputFileName = CSSBuilderUtil.getOutputFileName(
rtlFileName, _outputDirName, StringPool.BLANK);
}
else {
outputFileName = CSSBuilderUtil.getOutputFileName(
fileName, _outputDirName, StringPool.BLANK);
}
File outputFile = new File(_docrootDirName, outputFileName);
_write(outputFile, content);
File file = new File(_docrootDirName, fileName);
outputFile.setLastModified(file.lastModified());
}
private static RTLCSSConverter _rtlCSSConverter;
private final boolean _cleanPortalCommonDir;
private final String _docrootDirName;
private final boolean _generateSourceMap;
private final String _outputDirName;
private final String _portalCommonDirName;
private final int _precision;
private final Pattern[] _rtlExcludedPathPatterns;
private SassCompiler _sassCompiler;
}