package de.axone.web.servlet;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
import de.axone.cache.ng.CacheLRUMap;
import de.axone.cache.ng.CacheNG;
import de.axone.cache.ng.CacheNG.Cache;
import de.axone.cache.ng.CacheNG.Realm;
import de.axone.cache.ng.RealmImpl;
import de.axone.data.Charsets;
import de.axone.tools.EasyParser;
import de.axone.tools.Str;
import de.axone.tools.UrlParser;
import de.axone.tools.watcher.FileWatcher;
import de.axone.web.CssColorRotator;
import de.axone.web.Header;
import de.axone.web.ImgColorRotator;
public abstract class ResourcesServlet extends HttpServlet {
private static final String FAVICON_ICO = "favicon.ico";
private static final Logger log = LoggerFactory.getLogger( ResourcesServlet.class );
private static final Realm<String,Object> DEFAULT_RESOURCE_REALM = new RealmImpl<>( "resource cache" );
private static final long serialVersionUID = 1L;
private static final char FS = ';';
private static final String PREFIX = "/static";
private static final String P_DO_YUI = "yui";
private static final String P_NO_CACHE = "nc";
private static final float MIN_COMPRESSION = 0.90f;
private static final Pattern COLORIZE = Pattern.compile( "(/([cpgji]{1,5})\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)).*" );
private static final Pattern DEPLENK = Pattern.compile( "\\s*:\\s*" );
protected abstract File basedir();
protected abstract long cachetime();
protected Logger log(){ return log; }
protected String uriPrefix(){
return PREFIX;
}
protected String filter( HttpServletRequest req, HttpServletResponse resp, String uri ) {
if( uri.endsWith( FAVICON_ICO ) ){
uri = favicon( req, resp, uri );
}
/*
String prefix = uriPrefix();
if( uri.startsWith( prefix ) ){
uri = uri.substring( prefix.length() );
}
*/
uri = DEPLENK.matcher( uri ).replaceAll( ":" );
return uri;
}
protected String favicon( HttpServletRequest req, HttpServletResponse resp, String uri ) {
return uri; // Do nothing per default
}
private CacheNG.Cache<String,Object> cache = new CacheLRUMap<>( DEFAULT_RESOURCE_REALM, 1000 );
protected CacheNG.Cache<String,Object> buffer(){
return cache;
}
/**
* Overwrite in subclasses
*
* @param request
* @param uri
* @throws Exception
*/
protected void protect( HttpServletRequest request, String uri ) throws Exception {};
@Override
public void doGet( HttpServletRequest request, HttpServletResponse response ) {
try {
// Get settings
File basedir = basedir();
File subdir = null;
long cachetime = cachetime();
String requestUri = request.getRequestURI();
// Get requested resources
String uri = filter( request, response, requestUri );
protect( request, uri );
Cache<String,Object> buffer = buffer();
String pYui = request.getParameter( P_DO_YUI );
boolean doYui = !EasyParser.isNo( pYui );
String pNc = request.getParameter( P_NO_CACHE );
boolean doNotCache = pNc != null && pNc.length() == 0;
HttpDataHolder httpData = null;
synchronized( buffer ) {
String urlKey = uri + (doYui?"!":"");
// Is there sth. in the buffer?
if( !doNotCache && buffer.isCached( urlKey ) ) {
httpData = (HttpDataHolder) buffer.fetch( urlKey );
}
if( httpData == null || httpData.hasChanged() ){
String [] uriSplitted = Str.splitFast( uri, FS );
LinkedList<FileWatcher> watcherList = new LinkedList<FileWatcher>();
LinkedList<byte[]> datas = new LinkedList<byte[]>();
String basePath=null;
CssColorRotator rotCss = null;
ImgColorRotator rotImg = null;
for( String uriPart : uriSplitted ){
@SuppressWarnings( "unused" )
boolean isHtml=false, isCss=false, isJs=false, isPng=false, isJpg=false, isGif=false, isIcon=false;
if( uriPart.endsWith( ".html" ) || uriPart.endsWith( ".xhtml" ) ){
isHtml = true;
} else if( uriPart.endsWith( ".css" ) ){
isCss = true;
} else if( uriPart.endsWith( ".js" ) ){
isJs = true;
} else if( uriPart.endsWith( ".png" ) ){
isPng = true;
} else if( uriPart.endsWith( ".gif" ) ){
isGif = true;
} else if( uriPart.endsWith( ".jpg" )||uriPart.endsWith( ".jepg" ) ){
isJpg = true;
} else if( uriPart.endsWith( ".ico" ) ){
isIcon = true;
}
// Make colorizer
Matcher cm = COLORIZE.matcher( uriPart );
if( cm.matches() ){
String found = cm.group( 1 );
String mode = cm.group( 2 );
boolean doCss = isCss && mode.contains( "c" );
boolean doPng = isPng && mode.contains( "p" );
boolean doJpg = isJpg && mode.contains( "j" );
boolean doGif = isGif && mode.contains( "g" );
boolean inverse = mode.contains( "i" );
if( doCss || doPng || doJpg || doGif ){
Integer h = Integer.parseInt( cm.group( 3 ) );
Integer s = Integer.parseInt( cm.group( 4 ) );
Integer b = Integer.parseInt( cm.group( 5 ) );
if( doCss ){
rotCss = new CssColorRotator( h, s, b, inverse );
} else if( doPng || doJpg || doGif ){
rotImg = new ImgColorRotator( h, s, b, inverse );
}
}
uriPart = uriPart.substring( found.length() );
}
// For first save Path. All others will be relative
File file;
if( uriPart.charAt( 0 ) == '/' ){
if( uriPart.startsWith( uriPrefix() ) ){
uriPart = uriPart.substring( uriPrefix().length() );
}
File uriFile = new File( uriPart );
basePath = uriFile.getParent();
file = new File( basedir, uriPart );
subdir = new File( basedir, basePath );
} else {
if( subdir == null )
throw new IllegalArgumentException( "Missing basedir" );
file = new File( subdir, uriPart );
}
FileWatcher watcher = new FileWatcher( file );
watcher.haveChanged(); //reset watcher
if( !file.isFile() ) {
log().error( "Not found: " + uriPart );
response.sendError( HttpServletResponse.SC_NOT_FOUND, uriPart );
return;
}
// File size
long filesizeL = file.length();
if( filesizeL > Integer.MAX_VALUE ) {
log().error( "Too large: " + uriPart );
response.sendError(
HttpServletResponse.SC_NOT_IMPLEMENTED,
"File is too large: " + filesizeL + "B" );
return;
}
int filesize = (int) filesizeL;
// Read via nio
try (
FileInputStream fin = new FileInputStream( file );
FileChannel in = fin.getChannel();
) {
response.setContentLength( filesize );
ByteBuffer bb = in.map( FileChannel.MapMode.READ_ONLY, 0,
filesize );
byte[] plainData = new byte[ filesize ];
bb.get( plainData );
if( ( isCss || isJs ) && doYui ){
String dataAsString = new String( plainData, Charsets.utf8 );
if( isCss && rotCss != null ){
dataAsString = rotCss.rotate( dataAsString );
}
StringReader sIn = new StringReader( dataAsString );
StringWriter sOut = new StringWriter();
if( isCss ){
CssCompressor compressor = new CssCompressor( sIn );
compressor.compress( sOut, 0 );
} else {
JavaScriptCompressor compressor = new JavaScriptCompressor( sIn, null );
compressor.compress( sOut, 0, true, false, false, false );
}
String compressedString = sOut.toString();
plainData = compressedString.getBytes();
}
if( (isPng|isJpg|isGif) && rotImg != null ){
if( isPng ){
plainData = rotImg.rotate( plainData, "png" );
} else if( isJpg ){
plainData = rotImg.rotate( plainData, "jpeg" );
} else if( isGif ){
plainData = rotImg.rotate( plainData, "gif" );
}
}
datas.add( plainData );
watcherList.add( watcher );
}
}
int datasize=0;
for( byte[] d : datas ){
datasize += d.length;
}
ByteBuffer allData = ByteBuffer.allocate( datasize );
for( byte[] d : datas ){
allData.put( d );
}
allData.flip();
byte [] dataAsArray = allData.array();
httpData = new HttpDataHolder( dataAsArray, watcherList );
// Store doNotCache only if there is already an entry for that
// (in order to avoid wrong buffer entries)
if( !doNotCache || buffer.isCached( urlKey )){
buffer.put( urlKey, httpData );
}
}
}
// Regested E-Tag
String requestETag = request.getHeader( "If-None-Match" );
// E-Tag generated from hashing the data
String myETag = httpData.getETag();
// If matches send 304 (not modified) and no data
if( requestETag != null && requestETag.equals( myETag ) ){
// Don't send anything if request matches response
response.sendError( 304 );
return;
}
// Content-type vie uri / extension
String contentType = UrlParser.contentTypeFor( uri, true );
if( contentType == null ) {
log().error( "Unknown filetype: " + uri );
response.sendError( HttpServletResponse.SC_NOT_IMPLEMENTED,
"Unknown filetype" );
return;
}
response.setContentType( contentType );
// Encoding
String encoding = UrlParser.encodingFor( uri );
if( encoding != null ) {
response.setCharacterEncoding( encoding );
}
// Cache settings
if( doNotCache ){
response.setHeader( "Cache-Control", "no-cache, no-store, must-revalidate" );
response.setHeader( "Expires", "0" );
} else if( cachetime >= 0 ){
response.setHeader( "Cache-Control", "max-age="+cachetime );
}
// Proxies: Hold de-/compressed versions
response.setHeader( "Vary", "Accept-Encoding" );
// ETag
response.setHeader( "ETag", myETag );
String supportedCompression = request.getHeader( "Accept-Encoding" );
if(
( contentType.startsWith( "text/" ) || contentType.equals( "image/svg+xml" ) )
&& supportedCompression != null && supportedCompression.matches( ".*gzip.*" )
&& httpData.isGzipAvailable()
){
response.setHeader( "Content-Encoding", "gzip" );
byte [] data = httpData.getGziped();
response.setContentLength( data.length );
try( ServletOutputStream outs = response.getOutputStream(); ){
outs.write( data );
outs.flush();
}
} else {
response.setContentLength( httpData.getData().length );
try( ServletOutputStream outs = response.getOutputStream(); ){
outs.write( httpData.getData() );
outs.flush();
}
}
} catch( Throwable e ) {
log().error( "Error processing {}", request.getRequestURI(), e );
try {
onError( request, response, e );
} catch( Throwable t ){
log().error( "Error handling error", t );
t.printStackTrace();
}
}
}
protected void onError( HttpServletRequest req, HttpServletResponse resp, Throwable e ) throws Exception {
try {
resp.sendError( HttpServletResponse.SC_NOT_FOUND, e.getMessage() );
try ( PrintWriter o2 = resp.getWriter(); ){
e.printStackTrace( o2 );
} catch( IllegalStateException ise ){
ServletOutputStream sout = resp.getOutputStream();
try ( PrintStream otherOut = new PrintStream( sout, true, Charsets.utf8 ); ){
e.printStackTrace( otherOut );
}
}
} catch( IOException e1 ) {
e1.printStackTrace();
}
}
class HttpDataHolder {
private byte[] data;
private byte[] gziped;
private Boolean isGzipAvailable;
private String eTag;
private List<FileWatcher> watcher;
HttpDataHolder( byte[] data, List<FileWatcher> watcher ){
this.data = data;
this.watcher = watcher;
}
public byte[] getData() {
return data;
}
public byte[] getGziped(){
if( gziped == null && isGzipAvailable == null ){
try {
ByteArrayOutputStream bOut = new ByteArrayOutputStream( data.length );
GZIPOutputStream gOut = new GZIPOutputStream( bOut );
gOut.write( data );
gOut.finish();
gziped = bOut.toByteArray();
} catch( IOException e ) {
log().error( "Cannot write gziped content", e );
}
}
if( gziped != null && gziped.length > data.length * MIN_COMPRESSION ){
gziped = null;
isGzipAvailable = false;
} else {
isGzipAvailable = true;
}
return gziped;
}
public boolean isGzipAvailable(){
if( isGzipAvailable == null ){
getGziped();
}
return isGzipAvailable;
}
public String getETag() {
if( eTag == null ){
eTag = Header.makeETag( data );
}
return eTag;
}
public List<FileWatcher> getWatchers() {
return watcher;
}
public boolean hasChanged() {
boolean hasChanged = false;
for( FileWatcher watcher : getWatchers() ){
if( watcher.haveChanged() ){
hasChanged = true;
break;
}
}
return hasChanged;
}
}
}