package btools.server.request; import btools.router.RoutingContext; import btools.router.RoutingEngine; import btools.server.ServiceContext; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.Map; /** * Custom profile uploads */ public class ProfileUploadHandler { // maximum number of characters (file size limit for custom profiles) private static final int MAX_LENGTH = 100000; // prefix for custom profile id to distinguish from default profiles public static final String CUSTOM_PREFIX = "custom_"; public static final String SHARED_PREFIX = "shared_"; private ServiceContext serviceContext; public ProfileUploadHandler( ServiceContext serviceContext) { this.serviceContext = serviceContext; } public void handlePostRequest(String profileId, BufferedReader br, BufferedWriter response) throws IOException { BufferedWriter fileWriter = null; try { String id; if ( profileId != null ) { // update existing file when id appended id = profileId.substring( ProfileUploadHandler.CUSTOM_PREFIX.length() ); } else { id = "" + System.currentTimeMillis(); } File file = new File( getOrCreateCustomProfileDir(), id + ".brf" ); fileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream( file ) ) ); //StringWriter sw = new StringWriter(); bw = new BufferedWriter(sw); // only profile text as content readPostData(br, fileWriter, id); fileWriter.flush(); //System.out.println("data: |" + sw.toString() + "|"); Map responseData = new HashMap(); responseData.put( "profileid", CUSTOM_PREFIX + id ); validateProfile( id, responseData ); response.write( toJSON( responseData ) ); } finally { if ( fileWriter != null ) try { fileWriter.close(); } catch( Exception e ) {} } } private File getOrCreateCustomProfileDir() { // workaround: customProfileDir relative to profileDir, because RoutingEngine doesn't know custom profiles File customProfileDir = new File(serviceContext.profileDir, serviceContext.customProfileDir); if (!customProfileDir.exists()) { customProfileDir.mkdir(); } return customProfileDir; } // reads HTTP POST content from input into output stream/writer private static void readPostData( BufferedReader ir, BufferedWriter bw, String id ) throws IOException { // Content-Type: text/plain;charset=UTF-8 int numChars = 0; // Content-Length header is in bytes (!= characters for UTF8), // but Reader reads characters, so don't know number of characters to read for(;;) { // read will block when false, occurs at end of stream rather than -1 if (!ir.ready()) { try { Thread.sleep(1000); } catch( Exception e ) {} if ( !ir.ready() ) break; } int c = ir.read(); if ( c == -1) break; bw.write( c ); numChars++; if (numChars > MAX_LENGTH) throw new IOException("Maximum number of characters exceeded (" + MAX_LENGTH + ", " + id + ")"); } } private String toJSON( Map data ) { boolean first = true; StringBuilder sb = new StringBuilder(); sb.append("{"); for (Map.Entry entry : data.entrySet()) { sb.append(first ? "\n" : ",\n"); sb.append(" \""); sb.append(entry.getKey()); sb.append("\": \""); sb.append(entry.getValue()); sb.append("\""); first = false; } sb.append("\n}\n"); return sb.toString(); } public void validateProfile(String id, Map responseData) { // validate by initializing RoutingEngine, where parsing is done, and catching exceptions // see https://github.com/abrensch/brouter/issues/14 try { RoutingContext rc = new RoutingContext(); rc.localFunction = serviceContext.customProfileDir + "/" + id; new RoutingEngine( null, null, null, null, rc ); } catch ( Exception e ) { String msg = e.getMessage(); if ( msg == null ) { msg = ""; } else if ( msg.indexOf( "does not contain expressions for context" ) >= 0 ) { // remove absolute path in this specific exception, useful for server, but don't disclose to client msg = msg.substring( msg.indexOf( "does not contain expressions for context" ) ); } responseData.put( "error", "Profile error: " + msg ); } } }