001    /**
002     * Copyright (c) 2000-2013 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.tools;
016    
017    import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
018    import com.liferay.portal.kernel.io.unsync.UnsyncPrintWriter;
019    import com.liferay.portal.kernel.util.FastDateFormatFactoryUtil;
020    import com.liferay.portal.kernel.util.FileUtil;
021    import com.liferay.portal.kernel.util.PortalClassLoaderUtil;
022    import com.liferay.portal.kernel.util.PropsUtil;
023    import com.liferay.portal.kernel.util.StringPool;
024    import com.liferay.portal.kernel.util.StringUtil;
025    import com.liferay.portal.kernel.util.SystemProperties;
026    import com.liferay.portal.kernel.util.UnsyncPrintWriterPool;
027    import com.liferay.portal.kernel.util.Validator;
028    import com.liferay.portal.model.ModelHintsConstants;
029    import com.liferay.portal.scripting.ruby.RubyExecutor;
030    import com.liferay.portal.servlet.filters.aggregate.AggregateFilter;
031    import com.liferay.portal.servlet.filters.aggregate.FileAggregateContext;
032    import com.liferay.portal.servlet.filters.dynamiccss.RTLCSSUtil;
033    import com.liferay.portal.util.FastDateFormatFactoryImpl;
034    import com.liferay.portal.util.FileImpl;
035    import com.liferay.portal.util.PortalImpl;
036    import com.liferay.portal.util.PortalUtil;
037    import com.liferay.portal.util.PropsImpl;
038    
039    import java.io.File;
040    
041    import java.util.ArrayList;
042    import java.util.HashMap;
043    import java.util.List;
044    import java.util.Map;
045    
046    import org.apache.tools.ant.DirectoryScanner;
047    
048    /**
049     * @author Brian Wing Shun Chan
050     * @author Raymond Aug??
051     * @author Eduardo Lundgren
052     */
053    public class SassToCssBuilder {
054    
055            public static File getCacheFile(String fileName) {
056                    return getCacheFile(fileName, StringPool.BLANK);
057            }
058    
059            public static File getCacheFile(String fileName, String suffix) {
060                    return new File(getCacheFileName(fileName, suffix));
061            }
062    
063            public static String getCacheFileName(String fileName, String suffix) {
064                    String cacheFileName = StringUtil.replace(
065                            fileName, StringPool.BACK_SLASH, StringPool.SLASH);
066    
067                    int x = cacheFileName.lastIndexOf(StringPool.SLASH);
068    
069                    int y = cacheFileName.lastIndexOf(StringPool.PERIOD);
070    
071                    if (y == -1) {
072                            y = cacheFileName.length();
073                    }
074    
075                    return cacheFileName.substring(0, x + 1) + ".sass-cache/" +
076                            cacheFileName.substring(x + 1, y) + suffix +
077                                    cacheFileName.substring(y);
078            }
079    
080            public static String getContent(String docrootDirName, String fileName)
081                    throws Exception {
082    
083                    File file = new File(docrootDirName.concat(fileName));
084    
085                    String content = FileUtil.read(file);
086    
087                    content = AggregateFilter.aggregateCss(
088                            new FileAggregateContext(docrootDirName, fileName), content);
089    
090                    return parseStaticTokens(content);
091            }
092    
093            public static String getRtlCustomFileName(String fileName) {
094                    int pos = fileName.lastIndexOf(StringPool.PERIOD);
095    
096                    return fileName.substring(0, pos) + "_rtl" + fileName.substring(pos);
097            }
098    
099            public static void main(String[] args) throws Exception {
100                    Map<String, String> arguments = ArgumentsUtil.parseArguments(args);
101    
102                    List<String> dirNames = new ArrayList<String>();
103    
104                    String dirName = arguments.get("sass.dir");
105    
106                    if (Validator.isNotNull(dirName)) {
107                            dirNames.add(dirName);
108                    }
109                    else {
110                            for (int i = 0;; i++ ) {
111                                    dirName = arguments.get("sass.dir." + i);
112    
113                                    if (Validator.isNotNull(dirName)) {
114                                            dirNames.add(dirName);
115                                    }
116                                    else {
117                                            break;
118                                    }
119                            }
120                    }
121    
122                    String docrootDirName = arguments.get("sass.docroot.dir");
123                    String portalCommonDirName = arguments.get("sass.portal.common.dir");
124    
125                    new SassToCssBuilder(dirNames, docrootDirName, portalCommonDirName);
126            }
127    
128            public static String parseStaticTokens(String content) {
129                    return StringUtil.replace(
130                            content,
131                            new String[] {
132                                    "@model_hints_constants_text_display_height@",
133                                    "@model_hints_constants_text_display_width@",
134                                    "@model_hints_constants_textarea_display_height@",
135                                    "@model_hints_constants_textarea_display_width@"
136                            },
137                            new String[] {
138                                    ModelHintsConstants.TEXT_DISPLAY_HEIGHT,
139                                    ModelHintsConstants.TEXT_DISPLAY_WIDTH,
140                                    ModelHintsConstants.TEXTAREA_DISPLAY_HEIGHT,
141                                    ModelHintsConstants.TEXTAREA_DISPLAY_WIDTH
142                            });
143            }
144    
145            public SassToCssBuilder(
146                            List<String> dirNames, String docrootDirName,
147                            String portalCommonDirName)
148                    throws Exception {
149    
150                    Class<?> clazz = getClass();
151    
152                    ClassLoader classLoader = clazz.getClassLoader();
153    
154                    _initUtil(classLoader);
155    
156                    _rubyScript = StringUtil.read(
157                            classLoader,
158                            "com/liferay/portal/servlet/filters/dynamiccss" +
159                                    "/dependencies/main.rb");
160    
161                    _tempDir = SystemProperties.get(SystemProperties.TMP_DIR);
162    
163                    for (String dirName : dirNames) {
164    
165                            // Create a new Ruby executor as a workaround for a bug with Ruby
166                            // that breaks "ant build-css" when it parses too many CSS files
167    
168                            _rubyExecutor = new RubyExecutor();
169    
170                            _rubyExecutor.setExecuteInSeparateThread(false);
171    
172                            _parseSassDirectory(dirName, docrootDirName, portalCommonDirName);
173                    }
174            }
175    
176            private void _cacheSass(
177                            String docrootDirName, String portalCommonDirName, String fileName)
178                    throws Exception {
179    
180                    if (fileName.contains("_rtl") || RTLCSSUtil.isExcludedPath(fileName)) {
181                            return;
182                    }
183    
184                    File cacheFile = getCacheFile(docrootDirName.concat(fileName));
185    
186                    String parsedContent = _parseSassFile(
187                            docrootDirName, portalCommonDirName, fileName);
188    
189                    FileUtil.write(cacheFile, parsedContent);
190    
191                    File file = new File(docrootDirName.concat(fileName));
192    
193                    long lastModified = file.lastModified();
194    
195                    cacheFile.setLastModified(lastModified);
196    
197                    // Generate RTL cache
198    
199                    File rtlCacheFile = getCacheFile(
200                            docrootDirName.concat(fileName), "_rtl");
201    
202                    String rtlCss = RTLCSSUtil.getRtlCss(fileName, parsedContent);
203    
204                    // Append custom CSS for RTL
205    
206                    String rtlCustomFileName = getRtlCustomFileName(fileName);
207    
208                    File rtlCustomFile = new File(docrootDirName, rtlCustomFileName);
209    
210                    if (rtlCustomFile.exists()) {
211                            lastModified = rtlCustomFile.lastModified();
212    
213                            String rtlCustomCss = _parseSassFile(
214                                    docrootDirName, portalCommonDirName, rtlCustomFileName);
215    
216                            rtlCss += rtlCustomCss;
217                    }
218    
219                    FileUtil.write(rtlCacheFile, rtlCss);
220    
221                    rtlCacheFile.setLastModified(lastModified);
222            }
223    
224            private String _getCssThemePath(String fileName) {
225                    int pos = fileName.lastIndexOf("/css/");
226    
227                    return fileName.substring(0, pos + 4);
228            }
229    
230            private void _initUtil(ClassLoader classLoader) {
231                    FastDateFormatFactoryUtil fastDateFormatFactoryUtil =
232                            new FastDateFormatFactoryUtil();
233    
234                    fastDateFormatFactoryUtil.setFastDateFormatFactory(
235                            new FastDateFormatFactoryImpl());
236    
237                    FileUtil fileUtil = new FileUtil();
238    
239                    fileUtil.setFile(new FileImpl());
240    
241                    PortalClassLoaderUtil.setClassLoader(classLoader);
242    
243                    PortalUtil portalUtil = new PortalUtil();
244    
245                    portalUtil.setPortal(new PortalImpl());
246    
247                    PropsUtil.setProps(new PropsImpl());
248    
249                    RTLCSSUtil.init();
250            }
251    
252            private boolean _isModified(String dirName, String[] fileNames)
253                    throws Exception {
254    
255                    for (String fileName : fileNames) {
256                            if (fileName.contains("_rtl")) {
257                                    continue;
258                            }
259    
260                            fileName = _normalizeFileName(dirName, fileName);
261    
262                            File file = new File(fileName);
263                            File cacheFile = getCacheFile(fileName);
264    
265                            if (file.lastModified() != cacheFile.lastModified()) {
266                                    return true;
267                            }
268                    }
269    
270                    return false;
271            }
272    
273            private String _normalizeFileName(String dirName, String fileName) {
274                    return StringUtil.replace(
275                            dirName + StringPool.SLASH + fileName,
276                            new String[] {
277                                    StringPool.BACK_SLASH, StringPool.DOUBLE_SLASH
278                            },
279                            new String[] {
280                                    StringPool.SLASH, StringPool.SLASH
281                            }
282                    );
283            }
284    
285            private void _parseSassDirectory(
286                            String dirName, String docrootDirName, String portalCommonDirName)
287                    throws Exception {
288    
289                    DirectoryScanner directoryScanner = new DirectoryScanner();
290    
291                    String basedir = docrootDirName.concat(dirName);
292    
293                    directoryScanner.setBasedir(basedir);
294    
295                    directoryScanner.setExcludes(
296                            new String[] {
297                                    "**\\_diffs\\**", "**\\.sass-cache*\\**",
298                                    "**\\.sass_cache_*\\**", "**\\_sass_cache_*\\**",
299                                    "**\\_styled\\**", "**\\_unstyled\\**"
300                            });
301                    directoryScanner.setIncludes(new String[] {"**\\*.css"});
302    
303                    directoryScanner.scan();
304    
305                    String[] fileNames = directoryScanner.getIncludedFiles();
306    
307                    if (!_isModified(basedir, fileNames)) {
308                            return;
309                    }
310    
311                    for (String fileName : fileNames) {
312                            fileName = _normalizeFileName(dirName, fileName);
313    
314                            try {
315                                    long start = System.currentTimeMillis();
316    
317                                    _cacheSass(docrootDirName, portalCommonDirName, fileName);
318    
319                                    long end = System.currentTimeMillis();
320    
321                                    System.out.println(
322                                            "Parsed " + docrootDirName + fileName + " in " +
323                                                    (end - start) + " ms");
324                            }
325                            catch (Exception e) {
326                                    System.out.println("Unable to parse " + fileName);
327    
328                                    throw e;
329                            }
330                    }
331            }
332    
333            private String _parseSassFile(
334                            String docrootDirName, String portalCommonDirName, String fileName)
335                    throws Exception {
336    
337                    String filePath = docrootDirName.concat(fileName);
338    
339                    Map<String, Object> inputObjects = new HashMap<String, Object>();
340    
341                    inputObjects.put("commonSassPath", portalCommonDirName);
342                    inputObjects.put("content", getContent(docrootDirName, fileName));
343                    inputObjects.put("cssRealPath", filePath);
344                    inputObjects.put("cssThemePath", _getCssThemePath(filePath));
345                    inputObjects.put("sassCachePath", _tempDir);
346    
347                    UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
348                            new UnsyncByteArrayOutputStream();
349    
350                    UnsyncPrintWriter unsyncPrintWriter = UnsyncPrintWriterPool.borrow(
351                            unsyncByteArrayOutputStream);
352    
353                    inputObjects.put("out", unsyncPrintWriter);
354    
355                    _rubyExecutor.eval(null, inputObjects, null, _rubyScript);
356    
357                    unsyncPrintWriter.flush();
358    
359                    return unsyncByteArrayOutputStream.toString();
360            }
361    
362            private RubyExecutor _rubyExecutor;
363            private String _rubyScript;
364            private String _tempDir;
365    
366    }