package de.axone.web; import java.io.IOException; import java.io.Serializable; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.EnumMap; import java.util.EnumSet; import java.util.Set; import de.axone.data.Charsets; import de.axone.tools.A; import de.axone.tools.Str; import de.axone.web.SuperURL.Encode; import de.axone.web.SuperURL.FinalEncoding; import de.axone.web.SuperURL.Host; import de.axone.web.SuperURL.Part; import de.axone.web.SuperURL.Path; import de.axone.web.SuperURL.Query; import de.axone.web.SuperURL.Query.QueryPart; import de.axone.web.SuperURL.UserInfo; import de.axone.web.encoding.Encoder; import de.axone.web.encoding.Encoder_Attribute; import de.axone.web.encoding.Encoder_Html; import de.axone.web.encoding.TranslatingEncoder; public abstract class SuperURLPrinter implements Serializable { private static final long serialVersionUID = 1L; public static SuperURLPrinter Plain = new FastPlain(); public static SuperURLPrinter MinimalEncoded = new FastMinimalEncoded(); public static SuperURLPrinter FullEncoded = new FastFullEncoded(); public static SuperURLPrinter ForAttribute = MinimalEncoded .finishFor( FinalEncoding.Attribute ); public static SuperURLPrinter ForRedirect = MinimalEncoded; public abstract String toString( SuperURL url ); public abstract String toString( SuperURL.UserInfo userInfo ); public abstract String toString( SuperURL.Host host ); public abstract String toString( SuperURL.Path path ); public abstract String toString( SuperURL.Query query ); public abstract <W extends Appendable> W write( W result, SuperURL url ) throws IOException; public abstract <W extends Appendable> W write( W result, SuperURL.Path path ) throws IOException; public abstract <W extends Appendable> W write( W result, SuperURL.Query query ) throws IOException; public abstract <W extends Appendable> W write( W result, SuperURL.Host host ) throws IOException; public abstract <W extends Appendable> W write( W result, SuperURL.UserInfo userInfo ) throws IOException; public abstract SuperURLPrinter encode( SuperURL.Part part, SuperURL.Encode encode ); public abstract SuperURLPrinter encode( SuperURL.Encode encode ); public abstract SuperURLPrinter finishFor( FinalEncoding finalEncoding ); public abstract SuperURLPrinter exclude( Set<SuperURL.Part> parts ); public abstract SuperURLPrinter include( Set<SuperURL.Part> parts ); public abstract SuperURL.Encode encoding(); private static abstract class Abstract extends SuperURLPrinter { private static final long serialVersionUID = 1L; @Override public String toString( SuperURL url ){ try { return write( new StringWriter(), url ).toString(); } catch( IOException e ){ throw new RuntimeException( "SNAFU writing to StringWriter.", e ); } } @Override public String toString( SuperURL.Path path ){ try { return write( new StringWriter(), path ).toString(); } catch( IOException e ){ throw new RuntimeException( "SNAFU writing to StringWriter.", e ); } } @Override public String toString( SuperURL.Query query ){ try { return write( new StringWriter(), query ).toString(); } catch( IOException e ){ throw new RuntimeException( "SNAFU writing to StringWriter.", e ); } } @Override public String toString( SuperURL.Host host ){ try { return write( new StringWriter(), host ).toString(); } catch( IOException e ){ throw new RuntimeException( "SNAFU writing to StringWriter.", e ); } } @Override public String toString( SuperURL.UserInfo userInfo ){ try { return write( new StringWriter(), userInfo ).toString(); } catch( IOException e ){ throw new RuntimeException( "SNAFU writing to StringWriter.", e ); } } @Override public SuperURLPrinter encode( SuperURL.Part part, SuperURL.Encode encode ){ return new SuperURLPrinter.Custom().encode( encoding() ).encode( part, encode ); } @Override public SuperURLPrinter encode( SuperURL.Encode encode ){ return new SuperURLPrinter.Custom().encode( encoding() ).encode( encode ); } @Override public SuperURLPrinter finishFor( FinalEncoding finalEncoding ){ return new SuperURLPrinter.Custom().encode( encoding() ).finishFor( finalEncoding ); } @Override public SuperURLPrinter exclude( Set<SuperURL.Part> parts ){ return new SuperURLPrinter.Custom().encode( encoding() ).exclude( parts ); } @Override public SuperURLPrinter include( Set<SuperURL.Part> parts ){ return new SuperURLPrinter.Custom().encode( encoding() ).include( parts ); } } public static class Custom extends Abstract { private static final long serialVersionUID = 1L; public static EnumMap<Encode,FastPlain> ForEncode = new EnumMap<>( Encode.class ); static { ForEncode.put( Encode.Plain, (FastPlain)Plain ); ForEncode.put( Encode.Minimal, (FastPlain)MinimalEncoded ); ForEncode.put( Encode.Full, (FastPlain)FullEncoded ); } private Encode encode = Encode.Full; private EnumMap<Part,Encode> parts; private EnumSet<Part> include; @Override public SuperURLPrinter encode( SuperURL.Part part, SuperURL.Encode encode ){ if( parts == null ) parts = new EnumMap<>( Part.class ); this.parts.put( part, encode ); return this; } @Override public SuperURLPrinter encode( SuperURL.Encode encode ){ this.encode = encode; return this; } @Override public SuperURLPrinter finishFor( FinalEncoding finalEncoding ){ return new Finisher( finalEncoding, this ); } @Override public SuperURLPrinter exclude( Set<SuperURL.Part> parts ){ if( include == null ) include = EnumSet.allOf( SuperURL.Part.class ); include.removeAll( parts ); return this; } @Override public SuperURLPrinter include( Set<SuperURL.Part> parts ){ if( include == null ) include = EnumSet.noneOf( SuperURL.Part.class ); include.addAll( parts ); return this; } private FastPlain printer( Part part ) { FastPlain result = null; if( parts != null ) result = ForEncode.get( part ); if( result == null ) result = ForEncode.get( encode ); return result; } private boolean isInclude( Part part ){ return include == null || include.contains( part ); } @Override public <W extends Appendable> W write( W result, SuperURL url ) throws IOException { if( url.scheme != null && isInclude( Part.Scheme ) ){ if( url.scheme != SuperURL.NOSCHEME ){ result.append( printer( Part.Scheme ).encodeScheme( url.scheme ) ); result.append( "://" ); } else { result.append( "//" ); } } if( url.userInfo != null && isInclude( Part.UserInfo ) ){ write( result, url.userInfo ); result.append( '@' ); } if( url.host != null && isInclude( Part.Host ) ){ write( result, url.host ); } if( url.port != null && isInclude( Part.Port ) ){ result.append( ':' ); result.append( Integer.toString( url.port ) ); } if( url.path != null && isInclude( Part.Path ) ){ if( ! url.path.startsWithSlash && url.path.length() > 0 ) result.append( '/' ); write( result, url.path ); } if( url.query != null && url.query.size() > 0 && isInclude( Part.Query ) ){ result.append( '?' ); write( result, url.query ); } ; if( url.fragment != null && isInclude( Part.Fragment ) ){ String fragmentStr = url.fragment; fragmentStr = printer( Part.Fragment ).encodeFragment( fragmentStr ); result.append( '#' ).append( fragmentStr ); } return result; } @Override public <W extends Appendable> W write( W result, Path path ) throws IOException { return printer( Part.Path ).write( result, path ); } @Override public <W extends Appendable> W write( W result, Query query ) throws IOException { return printer( Part.Query ).write( result, query ); } @Override public <W extends Appendable> W write( W result, Host host ) throws IOException { return printer( Part.Host ).write( result, host ); } @Override public <W extends Appendable> W write( W result, UserInfo userInfo ) throws IOException { return printer( Part.UserInfo ).write( result, userInfo ); } @Override public Encode encoding() { return encode; } } public static class FastPlain extends Abstract { private static final long serialVersionUID = 1L; @Override public <W extends Appendable> W write( W result, SuperURL url ) throws IOException{ if( url.scheme != null ){ result.append( encodeScheme( url.scheme ) ); result.append( "://" ); } if( url.userInfo != null ){ write( result, url.userInfo ); result.append( '@' ); } if( url.host != null ){ write( result, url.host ); } if( url.port != null ){ result.append( ':' ); result.append( Integer.toString( url.port ) ); } if( url.path != null ){ if( ! url.path.startsWithSlash && url.path.length() > 0 ) result.append( '/' ); write( result, url.path ); } if( url.query != null && url.query.size() > 0 ){ result.append( '?' ); write( result, url.query ); } if( url.fragment != null ){ String fragmentStr = url.fragment; fragmentStr = encodeFragment( fragmentStr ); result.append( '#' ).append( fragmentStr ); } return result; } @Override public <W extends Appendable> W write( W result, Path path ) throws IOException { if( path.startsWithSlash ) result.append( '/' ); boolean first = true; for( String part : path ){ part = encodePath( part ); if( first ) first = false; else result.append( '/' ); result.append( part ); } // Now we need to prevent double slashes if starts && ends and no content if( path.endsWithSlash && ( ! path.startsWithSlash || path.path.size() > 0 ) ) result.append( '/' ); return result; } @Override public <W extends Appendable> W write( W result, Host host ) throws IOException { boolean first = true; for( String part : host.parts ){ part = encodeHost( part ); if( first ) first = false; else result.append( '.' ); result.append( part ); } return result; } @Override public <W extends Appendable> W write( W result, Query query ) throws IOException { boolean first = true; for( QueryPart part : query.path ){ if( first ) first = false; else result.append( '&' ); String keyStr = part.key; String valStr = part.value; if( keyStr != null ) keyStr = encodeQueryKey( keyStr ); if( valStr != null ) valStr = encodeQueryValue( valStr ); if( keyStr != null ){ result.append( keyStr ); } if( valStr != null ){ result.append( '=' ).append( valStr ); } } return result; } @Override public <W extends Appendable> W write( W result, UserInfo path ) throws IOException { String userStr = path.user; String passStr = path.pass; if( userStr != null ) userStr = encodeUserInfo( userStr ); if( passStr != null ) passStr = encodeUserInfo( passStr ); if( userStr != null ){ result.append( userStr ); } if( passStr != null ){ result.append( ':' ).append( passStr ); } return result; } protected String encodeUserInfo( String value ) { return encode( value ); } protected String encodeFragment( String value ) { return encode( value ); } protected String encodeScheme( String value ) { return encode( value ); } protected String encodeQueryValue( String value ) { return encode( value ); } protected String encodeQueryKey( String value ) { return encode( value ); } protected String encodeHost( String value ) { return encode( value ); } protected String encodePath( String value ) { return encode( value ); } protected String encode( String value ) { // HOPE: The vm will optimize this return value; // Dont encode } @Override public Encode encoding() { return Encode.Plain; } } public static class FastFullEncoded extends FastPlain { private static final long serialVersionUID = 1L; @Override protected String encode( String value ){ try { return Str.translate( URLEncoder.encode( value, Charsets.utf8 ), '+', "%20" ); } catch( UnsupportedEncodingException e ) { throw new RuntimeException( "SNAFU encoding", e ); } } @Override public Encode encoding() { return Encode.Full; } } public static class FastMinimalEncoded extends FastPlain { private static final long serialVersionUID = 1L; private static final char [] gen_delims = new char [] { '%', ':', '/', '?', '#', '[', ']', '@', '+', ' ', '"' }; private static final Encoder DEFAULT = buildEncoder( gen_delims );//, SpaceAs.percent ); private static final Encoder DEFAULT_plus_path = buildEncoder( gen_delims );//, SpaceAs.keep ); private static final Encoder DEFAULT_plus_query_key = buildEncoder( A.union( gen_delims, '&', '=' ) );//, SpaceAs.plus ); private static final Encoder DEFAULT_plus_query_value = buildEncoder( A.union( gen_delims, '&' ) );//, SpaceAs.plus ); private static final Encoder DEFAULT_plus_host = buildEncoder( A.union( gen_delims, '.' ) );//, SpaceAs.percent ); @Override protected String encodeUserInfo( String value ) { return DEFAULT.encode( value ); } @Override protected String encodeFragment( String value ) { return DEFAULT.encode( value ); } @Override protected String encodeScheme( String value ) { return DEFAULT.encode( value ); } @Override protected String encodeQueryValue( String value ) { return DEFAULT_plus_query_value.encode( value ); } @Override protected String encodeQueryKey( String value ) { return DEFAULT_plus_query_key.encode( value ); } @Override protected String encodeHost( String value ) { return DEFAULT_plus_host.encode( value ); } @Override protected String encodePath( String value ) { return DEFAULT_plus_path.encode( value ); } @Override public Encode encoding() { return Encode.Minimal; } } /* private enum SpaceAs{ keep, plus, percent; } */ private static TranslatingEncoder buildEncoder( char [] chars ){ char [] src = new char[ chars.length +1 ]; String [] target = new String[ chars.length +1 ]; for( int i=0; i<chars.length; i++ ){ char c = chars[ i ]; src[ i ] = c; target[ i ] = String.format( "%%%02x", (int)c ); } return new TranslatingEncoder( src, target ); } public static class Wrapper extends SuperURLPrinter{ private static final long serialVersionUID = 1L; protected final SuperURLPrinter wrapped; public Wrapper( SuperURLPrinter wrapped ){ this.wrapped = wrapped; } @Override public String toString( SuperURL url ) { return wrapped.toString( url ); } @Override public String toString( UserInfo userInfo ) { return wrapped.toString( userInfo ); } @Override public String toString( Host host ) { return wrapped.toString( host ); } @Override public String toString( Path path ) { return wrapped.toString( path ); } @Override public String toString( Query query ) { return wrapped.toString( query ); } @Override public <W extends Appendable> W write( W result, SuperURL url ) throws IOException { return wrapped.write( result, url ); } @Override public <W extends Appendable> W write( W result, Path path ) throws IOException { return wrapped.write( result, path ); } @Override public <W extends Appendable> W write( W result, Query query ) throws IOException { return wrapped.write( result, query ); } @Override public <W extends Appendable> W write( W result, Host host ) throws IOException { return wrapped.write( result, host ); } @Override public <W extends Appendable> W write( W result, UserInfo userInfo ) throws IOException { return wrapped.write( result, userInfo ); } @Override public SuperURLPrinter encode( Part part, Encode encode ) { return wrapped.encode( part, encode ); } @Override public SuperURLPrinter encode( Encode encode ) { return wrapped.encode( encode ); } @Override public SuperURLPrinter finishFor( FinalEncoding finalEncoding ) { return wrapped.finishFor( finalEncoding ); } @Override public SuperURLPrinter exclude( Set<Part> parts ) { return wrapped.exclude( parts ); } @Override public SuperURLPrinter include( Set<Part> parts ) { return wrapped.include( parts ); } @Override public Encode encoding() { return wrapped.encoding(); } } public static final class Finisher extends SuperURLPrinter.Wrapper { private static final long serialVersionUID = 1L; private static EnumMap<FinalEncoding,Encoder> ENCODER = new EnumMap<>( FinalEncoding.class ); static { //ENCODER.put( FinalEncoding.Plain, NoEncoder.instance() ); ENCODER.put( FinalEncoding.Html, Encoder_Html.instance() ); ENCODER.put( FinalEncoding.Attribute, Encoder_Attribute.instance() ); } private FinalEncoding finalEncoding; public Finisher( FinalEncoding finalEncoding, SuperURLPrinter wrapped ) { super( wrapped ); this.finalEncoding = finalEncoding; } @Override public String toString( SuperURL url ){ try { return write( new StringWriter(), url ).toString(); } catch( IOException e ){ throw new RuntimeException( "SNAFU writing to StringWriter.", e ); } } @Override public <W extends Appendable> W write( W result, SuperURL url ) throws IOException { Appendable out = result; if( finalEncoding != FinalEncoding.Plain ) out = ENCODER.get( finalEncoding ).filter( out ); wrapped.write( out, url ); return result; } @Override public SuperURLPrinter finishFor( FinalEncoding finalEncoding ) { this.finalEncoding = finalEncoding; return this; } } }