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