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.util.FastDateFormatFactoryImpl;
033    import com.liferay.portal.util.FileImpl;
034    import com.liferay.portal.util.PortalImpl;
035    import com.liferay.portal.util.PortalUtil;
036    import com.liferay.portal.util.PropsImpl;
037    
038    import java.io.File;
039    
040    import java.util.ArrayList;
041    import java.util.HashMap;
042    import java.util.List;
043    import java.util.Map;
044    
045    import org.apache.tools.ant.DirectoryScanner;
046    
047    /**
048     * @author Brian Wing Shun Chan
049     * @author Raymond Aug??
050     * @author Eduardo Lundgren
051     */
052    public class SassToCssBuilder {
053    
054            public static File getCacheFile(String fileName) {
055                    fileName = StringUtil.replace(
056                            fileName, StringPool.BACK_SLASH, StringPool.SLASH);
057    
058                    int pos = fileName.lastIndexOf(StringPool.SLASH);
059    
060                    String cacheFileName =
061                            fileName.substring(0, pos + 1) + ".sass-cache/" +
062                                    fileName.substring(pos + 1);
063    
064                    return new File(cacheFileName);
065            }
066    
067            public static void main(String[] args) throws Exception {
068                    Map<String, String> arguments = ArgumentsUtil.parseArguments(args);
069    
070                    List<String> dirNames = new ArrayList<String>();
071    
072                    String dirName = arguments.get("sass.dir");
073    
074                    if (Validator.isNotNull(dirName)) {
075                            dirNames.add(dirName);
076                    }
077                    else {
078                            for (int i = 0;; i++ ) {
079                                    dirName = arguments.get("sass.dir." + i);
080    
081                                    if (Validator.isNotNull(dirName)) {
082                                            dirNames.add(dirName);
083                                    }
084                                    else {
085                                            break;
086                                    }
087                            }
088                    }
089    
090                    String docrootDirName = arguments.get("sass.docroot.dir");
091                    String portalCommonDirName = arguments.get("sass.portal.common.dir");
092    
093                    new SassToCssBuilder(dirNames, docrootDirName, portalCommonDirName);
094            }
095    
096            public static String parseStaticTokens(String content) {
097                    return StringUtil.replace(
098                            content,
099                            new String[] {
100                                    "@model_hints_constants_text_display_height@",
101                                    "@model_hints_constants_text_display_width@",
102                                    "@model_hints_constants_textarea_display_height@",
103                                    "@model_hints_constants_textarea_display_width@"
104                            },
105                            new String[] {
106                                    ModelHintsConstants.TEXT_DISPLAY_HEIGHT,
107                                    ModelHintsConstants.TEXT_DISPLAY_WIDTH,
108                                    ModelHintsConstants.TEXTAREA_DISPLAY_HEIGHT,
109                                    ModelHintsConstants.TEXTAREA_DISPLAY_WIDTH
110                            });
111            }
112    
113            public SassToCssBuilder(
114                            List<String> dirNames, String docrootDirName,
115                            String portalCommonDirName)
116                    throws Exception {
117    
118                    Class<?> clazz = getClass();
119    
120                    ClassLoader classLoader = clazz.getClassLoader();
121    
122                    _initUtil(classLoader);
123    
124                    _rubyScript = StringUtil.read(
125                            classLoader,
126                            "com/liferay/portal/servlet/filters/dynamiccss/main.rb");
127    
128                    _tempDir = SystemProperties.get(SystemProperties.TMP_DIR);
129    
130                    for (String dirName : dirNames) {
131    
132                            // Create a new Ruby executor as a workaround for a bug with Ruby
133                            // that breaks "ant build-css" when it parses too many CSS files
134    
135                            _rubyExecutor = new RubyExecutor();
136    
137                            _rubyExecutor.setExecuteInSeparateThread(false);
138    
139                            _parseSassDirectory(dirName, docrootDirName, portalCommonDirName);
140                    }
141            }
142    
143            private String _getContent(String docrootDirName, String fileName)
144                    throws Exception {
145    
146                    File file = new File(docrootDirName.concat(fileName));
147    
148                    String content = FileUtil.read(file);
149    
150                    content = AggregateFilter.aggregateCss(
151                            new FileAggregateContext(docrootDirName, fileName), content);
152    
153                    return parseStaticTokens(content);
154            }
155    
156            private String _getCssThemePath(String fileName) {
157                    int pos = fileName.lastIndexOf("/css/");
158    
159                    return fileName.substring(0, pos + 4);
160            }
161    
162            private void _initUtil(ClassLoader classLoader) {
163                    FastDateFormatFactoryUtil fastDateFormatFactoryUtil =
164                            new FastDateFormatFactoryUtil();
165    
166                    fastDateFormatFactoryUtil.setFastDateFormatFactory(
167                            new FastDateFormatFactoryImpl());
168    
169                    FileUtil fileUtil = new FileUtil();
170    
171                    fileUtil.setFile(new FileImpl());
172    
173                    PortalClassLoaderUtil.setClassLoader(classLoader);
174    
175                    PortalUtil portalUtil = new PortalUtil();
176    
177                    portalUtil.setPortal(new PortalImpl());
178    
179                    PropsUtil.setProps(new PropsImpl());
180            }
181    
182            private boolean _isModified(String dirName, String[] fileNames)
183                    throws Exception {
184    
185                    for (String fileName : fileNames) {
186                            if (fileName.contains("_rtl")) {
187                                    continue;
188                            }
189    
190                            fileName = _normalizeFileName(dirName, fileName);
191    
192                            File file = new File(fileName);
193                            File cacheFile = getCacheFile(fileName);
194    
195                            if (file.lastModified() != cacheFile.lastModified()) {
196                                    return true;
197                            }
198                    }
199    
200                    return false;
201            }
202    
203            private String _normalizeFileName(String dirName, String fileName) {
204                    return StringUtil.replace(
205                            dirName + StringPool.SLASH + fileName,
206                            new String[] {
207                                    StringPool.BACK_SLASH, StringPool.DOUBLE_SLASH
208                            },
209                            new String[] {
210                                    StringPool.SLASH, StringPool.SLASH
211                            }
212                    );
213            }
214    
215            private void _parseSassDirectory(
216                            String dirName, String docrootDirName, String portalCommonDirName)
217                    throws Exception {
218    
219                    DirectoryScanner directoryScanner = new DirectoryScanner();
220    
221                    String basedir = docrootDirName.concat(dirName);
222    
223                    directoryScanner.setBasedir(basedir);
224    
225                    directoryScanner.setExcludes(
226                            new String[] {
227                                    "**\\_diffs\\**", "**\\.sass-cache*\\**",
228                                    "**\\.sass_cache_*\\**", "**\\_sass_cache_*\\**",
229                                    "**\\_styled\\**", "**\\_unstyled\\**"
230                            });
231                    directoryScanner.setIncludes(new String[] {"**\\*.css"});
232    
233                    directoryScanner.scan();
234    
235                    String[] fileNames = directoryScanner.getIncludedFiles();
236    
237                    if (!_isModified(basedir, fileNames)) {
238                            return;
239                    }
240    
241                    for (String fileName : fileNames) {
242                            fileName = _normalizeFileName(dirName, fileName);
243    
244                            try {
245                                    long start = System.currentTimeMillis();
246    
247                                    _parseSassFile(docrootDirName, portalCommonDirName, fileName);
248    
249                                    long end = System.currentTimeMillis();
250    
251                                    System.out.println(
252                                            "Parsed " + docrootDirName + fileName + " in " +
253                                                    (end - start) + " ms");
254                            }
255                            catch (Exception e) {
256                                    System.out.println("Unable to parse " + fileName);
257    
258                                    throw e;
259                            }
260                    }
261            }
262    
263            private void _parseSassFile(
264                            String docrootDirName, String portalCommonDirName, String fileName)
265                    throws Exception {
266    
267                    String filePath = docrootDirName.concat(fileName);
268    
269                    File file = new File(filePath);
270                    File cacheFile = getCacheFile(filePath);
271    
272                    Map<String, Object> inputObjects = new HashMap<String, Object>();
273    
274                    inputObjects.put("commonSassPath", portalCommonDirName);
275                    inputObjects.put("content", _getContent(docrootDirName, fileName));
276                    inputObjects.put("cssRealPath", filePath);
277                    inputObjects.put("cssThemePath", _getCssThemePath(filePath));
278                    inputObjects.put("sassCachePath", _tempDir);
279    
280                    UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
281                            new UnsyncByteArrayOutputStream();
282    
283                    UnsyncPrintWriter unsyncPrintWriter = UnsyncPrintWriterPool.borrow(
284                            unsyncByteArrayOutputStream);
285    
286                    inputObjects.put("out", unsyncPrintWriter);
287    
288                    _rubyExecutor.eval(null, inputObjects, null, _rubyScript);
289    
290                    unsyncPrintWriter.flush();
291    
292                    String parsedContent = unsyncByteArrayOutputStream.toString();
293    
294                    FileUtil.write(cacheFile, parsedContent);
295    
296                    cacheFile.setLastModified(file.lastModified());
297            }
298    
299            private RubyExecutor _rubyExecutor;
300            private String _rubyScript;
301            private String _tempDir;
302    
303    }