/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2014 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.server.ngclient.startup.resourceprovider;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.Manifest;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.osgi.framework.Bundle;
import org.sablo.specification.NGPackage;
import org.sablo.specification.NGPackage.IPackageReader;
import org.sablo.specification.WebComponentSpecProvider;
import org.sablo.specification.WebServiceSpecProvider;
import org.sablo.websocket.WebsocketSessionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.servoy.j2db.server.ngclient.WebsocketSessionFactory;
import com.servoy.j2db.server.ngclient.property.types.Types;
import com.servoy.j2db.server.ngclient.startup.Activator;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.Utils;
/**
* Filter that should only be there in a developer environment.
*
* @author jcompagner
*/
@WebFilter(urlPatterns = { "/*" })
public class ResourceProvider implements Filter
{
private static final Logger log = LoggerFactory.getLogger(ResourceProvider.class.getCanonicalName());
private static final Map<String, IPackageReader> componentReaders = new ConcurrentHashMap<>();
private static final Map<String, IPackageReader> serviceReaders = new ConcurrentHashMap<>();
private static final List<String> removePackageNames = new ArrayList<String>();
/**
* @param reader
* @return
*/
private static String getName(IPackageReader reader)
{
String name = reader.getName();
int index = name.lastIndexOf('/');
if (index == -1) index = name.lastIndexOf('\\');
if (index != -1) name = name.substring(index + 1);
// strip off the zip or jar extension
if (name.toLowerCase().endsWith(".jar") || name.toLowerCase().endsWith(".zip"))
{
name = name.substring(0, name.length() - 4);
}
return name;
}
public static void addComponentResources(Collection<IPackageReader> readers)
{
for (IPackageReader reader : readers)
{
componentReaders.put(getName(reader), reader);
}
initSpecProvider();
}
public static void refreshComponentResources(Collection<IPackageReader> readers)
{
removeComponentResources(readers);
initSpecProvider();
}
public static void removeComponentResources(Collection<IPackageReader> readers)
{
for (IPackageReader reader : readers)
{
componentReaders.remove(getName(reader));
}
}
public static void addServiceResources(Collection<IPackageReader> readers)
{
for (IPackageReader reader : readers)
{
serviceReaders.put(getName(reader), reader);
}
initSpecProvider();
}
public static void refreshServiceResources(Collection<IPackageReader> readers)
{
removeServiceResources(readers);
initSpecProvider();
}
public static void removeServiceResources(Collection<IPackageReader> readers)
{
for (IPackageReader reader : readers)
{
serviceReaders.remove(getName(reader));
}
}
public static Set<String> getDefaultPackageNames()
{
Set<String> result = new HashSet<String>();
Enumeration<String> paths = Activator.getContext().getBundle().getEntryPaths("/war/");
while (paths.hasMoreElements())
{
String name = paths.nextElement().replace("war/", "");
if (name.endsWith("/") && !name.equals("js/") && !name.equals("css/") && !name.equals("templates/"))
{
result.add(name.replace("/", ""));
}
}
return result;
}
public static void setRemovedPackages(List<String> packageNames)
{
removePackageNames.clear();
removePackageNames.addAll(packageNames);
}
private synchronized static void initSpecProvider()
{
//register the session factory at the manager
if (WebsocketSessionManager.getWebsocketSessionFactory(WebsocketSessionFactory.CLIENT_ENDPOINT) == null)
{
WebsocketSessionManager.setWebsocketSessionFactory(WebsocketSessionFactory.CLIENT_ENDPOINT, new WebsocketSessionFactory());
}
registerTypes();
List<IPackageReader> componentPackages = new ArrayList<>(componentReaders.values());
List<IPackageReader> servicePackages = new ArrayList<>(serviceReaders.values());
for (URL url : Utils.iterate(Activator.getContext().getBundle().findEntries("/war/", "MANIFEST.MF", true)))
{
String pathPrefix = url.getPath().substring(0, url.getPath().indexOf("/META-INF/MANIFEST.MF"));
IPackageReader reader = new BundlePackageReader(Activator.getContext().getBundle(), url, pathPrefix);
if (removePackageNames.contains(reader.getPackageName())) continue;
if (pathPrefix.endsWith("services"))
{
servicePackages.add(reader);
}
else
{
componentPackages.add(reader);
}
}
// Add sablo services and components
Bundle sabloBundle = org.sablo.startup.Activator.getDefault().getContext().getBundle();
BundlePackageReader sabloReader = new BundlePackageReader(sabloBundle, sabloBundle.getEntry("META-INF/MANIFEST.MF"), "META-INF/resources");
servicePackages.add(sabloReader);
componentPackages.add(sabloReader);
WebComponentSpecProvider.init(componentPackages.toArray(new IPackageReader[componentPackages.size()]));
WebServiceSpecProvider.init(servicePackages.toArray(new IPackageReader[servicePackages.size()]));
}
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
initSpecProvider();
}
private static void registerTypes()
{
Types.getTypesInstance().registerTypes();
}
@SuppressWarnings("nls")
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
String pathInfo = ((HttpServletRequest)request).getRequestURI();
if (pathInfo != null && !pathInfo.equals("/"))
{
Bundle bundle;
try
{
bundle = Activator.getContext().getBundle();
}
catch (IllegalStateException e)
{
// Context not valid
chain.doFilter(request, response);
return;
}
URL url = computeURL(pathInfo, bundle);
if (url != null)
{
URLConnection connection = url.openConnection();
connection.setUseCaches(false);
long lastModifiedTime = connection.getLastModified() / 1000 * 1000;
((HttpServletResponse)response).setDateHeader("Last-Modified", lastModifiedTime);
((HttpServletResponse)response).setHeader("Cache-Control", "max-age=0, must-revalidate, proxy-revalidate"); //HTTP 1.1
long lm = ((HttpServletRequest)request).getDateHeader("If-Modified-Since");
if (lm != -1 && lm == lastModifiedTime)
{
((HttpServletResponse)response).setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
response.setContentLength(connection.getContentLength());
if (connection.getContentType() != null && connection.getContentType().indexOf("unknown") == -1)
{
response.setContentType(connection.getContentType());
}
else
{
String file = url.getFile();
if (file.toLowerCase().endsWith(".js"))
{
response.setContentType("text/javascript");
}
else if (file.toLowerCase().endsWith(".css"))
{
response.setContentType("text/css");
}
else if (file.toLowerCase().endsWith(".html"))
{
response.setContentType("text/html");
}
}
String compileLessWithNashorn = null;
if (url.getFile().toLowerCase().endsWith(".less") && (compileLessWithNashorn = compileLessWithNashorn(url)) != null)
{
response.setContentType("text/css");
response.setContentLength(compileLessWithNashorn.length());
response.getWriter().print(compileLessWithNashorn);
}
else try (InputStream is = connection.getInputStream())
{
Utils.streamCopy(is, response.getOutputStream());
}
}
else
{
chain.doFilter(request, response);
}
}
else
{
chain.doFilter(request, response);
}
}
private static String getText(URL url) throws Exception
{
URLConnection connection = url.openConnection();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder result = new StringBuilder();
String inputLine;
while ((inputLine = bufferedReader.readLine()) != null)
result.append(inputLine);
bufferedReader.close();
return result.toString();
}
public static String compileLessWithNashorn(URL url)
{
//we have to pass in null as classloader if we want to acess the java 8 nashorn
ScriptEngine engine = new ScriptEngineManager(null).getEngineByName("nashorn");
if (engine != null)
{
try
{
Invocable invocable = (Invocable)engine;
invocable.invokeFunction("load", ResourceProvider.class.getResource("js/less-2.5.1.js"));
invocable.invokeFunction("load", ResourceProvider.class.getResource("js/less-env-2.5.1.js"));
invocable.invokeFunction("load", ResourceProvider.class.getResource("js/lessrunner.js"));
Object result = invocable.invokeFunction("convert", getText(url));
return result.toString();
}
catch (ScriptException e)
{
Debug.log(e);
}
catch (NoSuchMethodException e)
{
Debug.log(e);
}
catch (Exception e)
{
Debug.log(e);
}
}
return null;
}
/**
* @param pathInfo
* @param bundle
* @return
* @throws UnsupportedEncodingException
* @throws MalformedURLException
*/
public static URL computeURL(String pathInfo, Bundle bundle) throws UnsupportedEncodingException, MalformedURLException
{
URL url = null;
try
{
if (pathInfo.startsWith("/")) url = bundle.getEntry("/war" + pathInfo);
else url = bundle.getEntry("/war/" + pathInfo);
}
catch (Exception e)
{
Debug.log("can't get zip entry '" + pathInfo + "'from bundle: " + bundle, e);
}
if (url == null)
{
int index = pathInfo.indexOf('/', 1);
if (index > 1 && !pathInfo.substring(index).equals("/"))
{
String packageName = URLDecoder.decode(pathInfo.substring(0, index), "UTF8");
packageName = packageName.startsWith("/") ? packageName.substring(1) : packageName;
IPackageReader reader = componentReaders.get(packageName);
if (reader != null)
{
url = reader.getUrlForPath(pathInfo.substring(index));
if (url == null)
{
Debug.error("url '" + pathInfo.substring(index) + "' for package: '" + packageName + "' is not found in the component package");
}
}
else
{
reader = serviceReaders.get(packageName);
if (reader != null)
{
url = reader.getUrlForPath(pathInfo.substring(index));
if (url == null)
{
Debug.error("url '" + pathInfo.substring(index) + "' for package: '" + packageName + "' is not found in the service package");
}
}
}
}
}
if (url == null)
{
url = org.sablo.startup.Activator.getDefault().getResource(pathInfo);
}
return url;
}
@Override
public void destroy()
{
}
private static class BundlePackageReader implements NGPackage.IPackageReader
{
private final URL urlOfManifest;
private final Bundle bundle;
private final String pathPrefix;
public BundlePackageReader(Bundle bundle, URL urlOfManifest, String pathPrefix)
{
this.bundle = bundle;
this.urlOfManifest = urlOfManifest;
this.pathPrefix = pathPrefix;
}
@Override
public String getName()
{
return urlOfManifest.toExternalForm();
}
@Override
public String getPackageName()
{
try
{
String packageName = NGPackage.getPackageName(getManifest());
if (packageName != null) return packageName;
}
catch (IOException e)
{
Debug.log(e);
}
// fall back to based on directory name
String[] split = pathPrefix.split("/");
return split[split.length - 1];
}
@Override
public String getPackageDisplayname()
{
try
{
String packageDisplayname = NGPackage.getPackageDisplayname(getManifest());
if (packageDisplayname != null) return packageDisplayname;
}
catch (IOException e)
{
Debug.log(e);
}
// fall back to symbolic name
return getPackageName();
}
@Override
public Manifest getManifest() throws IOException
{
try (InputStream is = urlOfManifest.openStream())
{
return new Manifest(is);
}
}
@Override
public URL getUrlForPath(String path)
{
// when pathprefix is already in path, do not add pathprefix
return bundle.getEntry(path.startsWith(pathPrefix) ? path : pathPrefix + (path.startsWith("/") ? path : '/' + path));
}
@Override
public String readTextFile(String path, Charset charset) throws IOException
{
URL url = getUrlForPath(path);
if (url == null) return null;
try (InputStream is = url.openStream())
{
return Utils.getTXTFileContent(is, charset);
}
}
@Override
public void reportError(String specpath, Exception e)
{
log.error("Cannot parse spec file '" + specpath + "' from package 'BundlePackageReader[ " + urlOfManifest + " ]'. ", e);
}
@Override
public URL getPackageURL()
{
return null;
}
}
}