001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.utils;
019
020import io.stallion.exceptions.NotFoundException;
021import io.stallion.exceptions.UsageException;
022import io.stallion.plugins.StallionJavaPlugin;
023import io.stallion.plugins.PluginRegistry;
024import io.stallion.services.Log;
025import io.stallion.settings.Settings;
026import org.apache.commons.io.FilenameUtils;
027import org.apache.commons.io.IOUtils;
028import org.apache.commons.lang3.StringUtils;
029import org.parboiled.common.FileUtils;
030
031import java.io.*;
032import java.net.URL;
033import java.net.URLDecoder;
034import java.nio.charset.Charset;
035import java.util.*;
036import java.util.jar.JarEntry;
037import java.util.jar.JarFile;
038
039import static io.stallion.utils.Literals.*;
040
041
042public class ResourceHelpers {
043
044    public static String loadAssetResource(String pluginName, String path) throws IOException {
045        if (path.contains("..") || (!path.startsWith("/assets/") && path.startsWith("assets"))) {
046            throw new UsageException("Invalid path: " + path);
047        }
048        return loadResource(pluginName, path);
049    }
050
051    public static String loadTemplateResource(String pluginName, String path) throws IOException {
052        if (path.contains("..") || (!path.startsWith("/assets/") && path.startsWith("assets"))) {
053            throw new UsageException("Invalid path: " + path);
054        }
055        return loadResource(pluginName, path);
056    }
057
058    public static boolean resourceExists(String pluginName, String path) {
059        URL url = null;
060        try {
061            url = pluginPathToUrl(pluginName, path);
062        } catch (FileNotFoundException e) {
063            return false;
064        }
065        if (url == null) {
066            return false;
067        } else {
068            return true;
069        }
070    }
071
072    public static URL getUrlOrNotFound(String pluginName, String path) {
073        try {
074            URL url = pluginPathToUrl(pluginName, path);
075            if (url == null) {
076                throw new NotFoundException("Resource not found: " + pluginName + ":"  + path);
077            }
078            return url;
079        } catch(FileNotFoundException e) {
080            throw new NotFoundException("Resource not found: " + pluginName + ":"  + path);
081        }
082    }
083
084    public static String loadResource(String pluginName, String path) {
085        try {
086            URL url = pluginPathToUrl(pluginName, path);
087            if (url == null) {
088                throw new FileNotFoundException("Resource not found: " + path);
089            }
090            return loadResourceFromUrl(url, pluginName);
091        } catch (IOException e) {
092            throw new RuntimeException(e);
093        }
094    }
095
096    public static List<String> listFilesInDirectory(String plugin, String path) {
097        String ending = "";
098        String starting = "";
099        if (path.contains("*")) {
100            String[] parts = StringUtils.split(path, "*", 2);
101            String base = parts[0];
102            if (!base.endsWith("/")) {
103                path = new File(base).getParent();
104                starting = FilenameUtils.getName(base);
105            } else {
106                path = base;
107            }
108            ending = parts[1];
109        }
110
111        Log.info("listFilesInDirectory Parsed Path {0} starting:{1} ending:{2}", path, starting, ending);
112        URL url = PluginRegistry.instance().getJavaPluginByName().get(plugin).getClass().getResource(path);
113        Log.info("URL: {0}", url);
114
115        List<String> filenames = new ArrayList<>();
116        URL dirURL = getClassForPlugin(plugin).getResource(path);
117        Log.info("Dir URL is {0}", dirURL);
118        // Handle file based resource folder
119        if (dirURL != null && dirURL.getProtocol().equals("file")) {
120            String fullPath = dirURL.toString().substring(5);
121            File dir = new File(fullPath);
122            // In devMode, use the source resource folder, rather than the compiled version
123            if (Settings.instance().getDevMode()) {
124                String devPath = fullPath.replace("/target/classes/", "/src/main/resources/");
125                File devFolder = new File(devPath);
126                if (devFolder.exists()){
127                    dir = devFolder;
128                }
129            }
130            Log.info("List files from folder {0}", dir.getAbsolutePath());
131            List<String> files = list();
132            for (String name: dir.list()) {
133                if (!empty(ending) && !name.endsWith(ending)) {
134                    continue;
135                }
136                if (!empty(starting) && !name.endsWith("starting")) {
137                    continue;
138                }
139                // Skip special files, hidden files
140                if (name.startsWith(".") || name.startsWith("~") || name.startsWith("#") || name.contains("_flymake.")) {
141                    continue;
142                }
143                filenames.add(path + name);
144            }
145            return filenames;
146        }
147
148
149        if (dirURL.getProtocol().equals("jar")) {
150        /* A JAR path */
151            String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file
152            JarFile jar = null;
153            try {
154                jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"));
155            } catch (IOException e) {
156                throw new RuntimeException(e);
157            }
158            if (path.startsWith("/")) {
159                path = path.substring(1);
160            }
161            Enumeration<JarEntry> entries = jar.entries(); //gives ALL entries in jar
162            Set<String> result = new HashSet<String>(); //avoid duplicates in case it is a subdirectory
163            while(entries.hasMoreElements()) {
164                String name = entries.nextElement().getName();
165                Log.finer("Jar file entry: {0}", name);
166                if (name.startsWith(path)) { //filter according to the path
167                    String entry = name.substring(path.length());
168                    int checkSubdir = entry.indexOf("/");
169                    if (checkSubdir >= 0) {
170                        // if it is a subdirectory, we just return the directory name
171                        entry = entry.substring(0, checkSubdir);
172                    }
173                    if (!empty(ending) && !name.endsWith(ending)) {
174                        continue;
175                    }
176                    if (!empty(starting) && !name.endsWith("starting")) {
177                        continue;
178                    }
179                    // Skip special files, hidden files
180                    if (name.startsWith(".") || name.startsWith("~") || name.startsWith("#") || name.contains("_flymake.")) {
181                        continue;
182                    }
183                    result.add(entry);
184                }
185            }
186            return new ArrayList<>(result);
187        }
188        throw new UnsupportedOperationException("Cannot list files for URL "+dirURL);
189        /*
190        try {
191            URL url1 = getClassForPlugin(plugin).getResource(path);
192            Log.info("URL1 {0}", url1);
193            if (url1 != null) {
194                Log.info("From class folder contents {0}", IOUtils.toString(url1));
195                Log.info("From class folder contents as stream {0}", IOUtils.toString(getClassForPlugin(plugin).getResourceAsStream(path)));
196            }
197            URL url2 = getClassLoaderForPlugin(plugin).getResource(path);
198            Log.info("URL1 {0}", url2);
199            if (url2 != null) {
200                Log.info("From classLoader folder contents {0}", IOUtils.toString(url2));
201                Log.info("From classLoader folder contents as stream {0}", IOUtils.toString(getClassLoaderForPlugin(plugin).getResourceAsStream(path)));
202            }
203
204        } catch (IOException e) {
205            Log.exception(e, "error loading path " + path);
206        }
207        //  Handle jar based resource folder
208        try(
209                InputStream in = getResourceAsStream(plugin, path);
210                BufferedReader br = new BufferedReader( new InputStreamReader( in ) ) ) {
211            String resource;
212            while( (resource = br.readLine()) != null ) {
213                Log.finer("checking resource for inclusion in directory scan: {0}", resource);
214                if (!empty(ending) && !resource.endsWith(ending)) {
215                    continue;
216                }
217                if (!empty(starting) && !resource.endsWith("starting")) {
218                    continue;
219                }
220                // Skip special files, hidden files
221                if (resource.startsWith(".") || resource.startsWith("~") || resource.startsWith("#") || resource.contains("_flymake.")) {
222                    continue;
223                }
224                Log.finer("added resource during directory scan: {0}", resource);
225                filenames.add(path + resource);
226            }
227        } catch (IOException e) {
228            throw new RuntimeException(e);
229        }
230        return filenames;
231        */
232    }
233
234    private static InputStream getResourceAsStream(String plugin, String resource ) {
235
236        if (empty(plugin) || plugin.equals("stallion")) {
237            return ResourceHelpers.class.getClassLoader().getResourceAsStream(resource);
238        } else {
239            return  PluginRegistry.instance().getJavaPluginByName().get(plugin).getClass().getClassLoader().getResourceAsStream(resource);
240        }
241    }
242
243    private static ClassLoader getClassLoaderForPlugin(String plugin) {
244        if (empty(plugin) || "stallion".equals(plugin)) {
245            return ResourceHelpers.class.getClassLoader();
246        } else {
247            return PluginRegistry.instance().getJavaPluginByName().get(plugin).getClass().getClassLoader();
248        }
249    }
250
251    private static Class getClassForPlugin(String plugin) {
252        if (empty(plugin) || "stallion".equals(plugin)) {
253            return ResourceHelpers.class;
254        } else {
255            return PluginRegistry.instance().getJavaPluginByName().get(plugin).getClass();
256        }
257    }
258
259
260    public static URL pluginPathToUrl(String pluginName, String path) throws FileNotFoundException {
261        URL url = null;
262        if ("stallion".equals(pluginName) || empty(pluginName)) {
263            url = ResourceHelpers.class.getResource(path);
264        } else {
265            StallionJavaPlugin booter = PluginRegistry.instance().getJavaPluginByName().get(pluginName);
266            if (booter == null) {
267                throw new FileNotFoundException("No plugin found: " + pluginName);
268            }
269            url = booter.getClass().getResource(path);
270        }
271        return url;
272    }
273
274    public static byte[] loadBinaryResource(String pluginName, String path) {
275        try {
276            URL url = pluginPathToUrl(pluginName, path);
277            if (url == null) {
278                throw new FileNotFoundException("Resource not found: " + path);
279            }
280            return loadBinaryResource(url, pluginName);
281        } catch (IOException e) {
282            throw new RuntimeException(e);
283        }
284    }
285
286    public static byte[] loadBinaryResource(URL resourceUrl, String pluginName)  {
287
288        try {
289            File file = urlToFileMaybe(resourceUrl, pluginName);
290            if (file == null) {
291                return IOUtils.toByteArray(resourceUrl.openStream());
292            } else {
293                return FileUtils.readAllBytes(file);
294            }
295        } catch (IOException e) {
296            throw new RuntimeException(e);
297        }
298    }
299
300    public static String loadResourceFromUrl(URL resourceUrl) throws IOException {
301        return loadResourceFromUrl(resourceUrl, "");
302    }
303
304    public static String loadResourceFromUrl(URL resourceUrl, String pluginName) throws IOException {
305        File file = urlToFileMaybe(resourceUrl, pluginName) ;
306        if (file != null) {
307            return FileUtils.readAllText(file, Charset.forName("UTF-8"));
308        } else {
309            return IOUtils.toString(resourceUrl, Charset.forName("UTF-8"));
310
311        }
312    }
313
314    public static File findDevModeFileForResource(String plugin, String resourcePath) {
315        if (!Settings.instance().getDevMode()) {
316            throw new UsageException("You can only call this method in dev mode!");
317        }
318        try {
319            if (!resourcePath.startsWith("/")) {
320                resourcePath = "/" + resourcePath;
321            }
322            // We find the URL of the root, not the actual file, since if the file was just created, we want it to be
323            // accessible even if mvn hasn't recompiled the project. This allows us to iterate more quickly.
324            URL url = pluginPathToUrl(plugin, resourcePath);
325            // Maybe the file was just created? We'll try to find the root folder path, and then add the file path
326            if (url == null) {
327                url = new URL(StringUtils.stripStart(url.toString(), "/") + resourcePath);
328            }
329            return urlToFileMaybe(url, plugin);
330        } catch (IOException e) {
331            throw new RuntimeException(e);
332        }
333    }
334
335    private static File urlToFileMaybe(URL resourceUrl, String pluginName) {
336        Log.finer("Load resource URL={0} plugin={1}", resourceUrl, pluginName);
337        if (!Settings.instance().getDevMode()) {
338            return null;
339        }
340
341        // If the resourceUrl points to a local file, and the file exists in the file system, then use that file
342        Log.finer("Resource URL  {0}", resourceUrl);
343        if (resourceUrl.toString().startsWith("file:/")) {
344            String path = resourceUrl.toString().substring(5).replace("/target/classes/", "/src/main/resources/");
345            File file = new File(path);
346            if (file.isFile()) {
347                Log.finest("Load resource from source path {0}", path);
348                return file;
349            }
350        }
351
352        String[] parts = resourceUrl.toString().split("!", 2);
353        if (parts.length < 2) {
354            return null;
355        }
356        String relativePath = parts[1];
357        if (!relativePath.startsWith("/")) {
358            relativePath = "/" + relativePath;
359        }
360        if (relativePath.contains("..")) {
361            throw new UsageException("Invalid characters in the URL path: " + resourceUrl.toString());
362        }
363
364        List<String> paths = list();
365        if (empty(pluginName) || "stallion".equals(pluginName)) {
366            paths.add(System.getProperty("user.home") + "/st/core/src/main/resources" + relativePath);
367            paths.add(System.getProperty("user.home") + "/stallion/core/src/main/resources" + relativePath);
368        } else {
369            paths.add(System.getProperty("user.home") + "/st/" + pluginName + "/src/main/resources" + relativePath);
370            paths.add(System.getProperty("user.home") + "/stallion/" + pluginName + "/src/main/resources" + relativePath);
371        }
372        boolean isText = true;
373
374        for (String path: paths) {
375            File file = new File(path);
376            if (file.isFile()) {
377                Log.finest("Load resource from guessed path {0}", path);
378                return file;
379            }
380        }
381
382        // All else fails, just return it based on the resourc
383        Log.finest("Could not find a devMode version for resource path {0}", resourceUrl.toString());
384        return null;
385
386    }
387}