All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.numdata.uri.URITools Maven / Gradle / Ivy

Go to download

Library to process URI's and implementation for 'ftp', 'serial', 'smb', and 'memory' schemes.

There is a newer version: 1.22
Show newest version
/*
 * Copyright (c) 2017, Numdata BV, The Netherlands.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Numdata nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL NUMDATA BV BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.numdata.uri;

import java.io.*;
import java.net.*;
import java.util.*;

import com.numdata.oss.*;
import com.numdata.oss.io.*;
import com.numdata.oss.log.*;
import com.numdata.serial.*;
import jcifs.smb.*;
import org.apache.commons.net.*;
import org.apache.commons.net.ftp.*;
import org.jetbrains.annotations.*;

/**
 * This class provides utility methods related to {@link URI}s. This class
 * supports all schemes supported by the JVM and explicitly supports the
 * schemes: file, ftp, smb, and serial.
 *
 * 

FTP

The following parameters can be used: * * (Non-standard parameters are marked with a '*'.) * * * * * * * * * * * * * * * * * *
KeyValueDescription
typeaSet ASCII transfer type.
typeiSet binary transfer type.
*modepassiveUse passive connection * mode.
*modeactiveUse active connection * mode.
*no-chdir Write files without using {@code * chdir} commands. Qualified filenames are used instead. Note that RFC 1738: Uniform Resource * Locators (URL) states that each path segment "is to be supplied, * sequentially, as the argument to a CWD (change working directory) command." *
*deleteDelete files before they are * written.
* * @author Peter S. Heijnen * @see URI Java URI definition * @see Javacomm#openSerialPort Definition of 'serial' scheme * @see SmbFile Definition of the 'smb' scheme */ public class URITools { /** * Log used for messages related to this class. */ private static final ClassLogger LOG = ClassLogger.getFor( URITools.class ); /** * Extended version of {@link URI#resolve(String)} method. This also * supports resolving against a 'jar:!/' URI. * * @param context URI context to resolve against. * @param uri URI to resolve against the context URI. * * @return The resulting URI. * * @see URI#resolve(URI, URI) */ public static URI resolve( final String context, final String uri ) { return resolve( URI.create( context ), URI.create( uri ) ); } /** * Extended version of {@link URI#resolve(String)} method. This also * supports resolving against a 'jar:!/' URI. * * @param context URI context to resolve against. * @param uri URI to resolve against the context URI. * * @return The resulting URI. * * @see URI#resolve(URI, URI) */ public static URI resolve( final URI context, final String uri ) { return resolve( context, URI.create( uri ) ); } /** * Extended version of {@link URI#resolve(URI)} method. This also supports * resolving against a 'jar:!/' URI. * * @param context URI context to resolve against. * @param uri URI to resolve against the context URI. * * @return The resulting URI. * * @see URI#resolve(URI, URI) */ public static URI resolve( final URI context, final URI uri ) { URI result = context.resolve( uri ); /* * Implement resolving against 'jar:!/' URI. */ if ( ( result == uri ) && !uri.isOpaque() && context.isOpaque() && "jar".equals( context.getScheme() ) ) { final String schemeSpecific = context.getRawSchemeSpecificPart(); final int separator = schemeSpecific.lastIndexOf( "!/" ); if ( separator > 0 ) { final URI path = URI.create( schemeSpecific.substring( separator + 1 ) ); final URI resolvedPath = resolve( path, uri ); if ( resolvedPath != uri ) { result = URI.create( context.getScheme() + ':' + schemeSpecific.substring( 0, separator + 1 ) + resolvedPath ); } } } return result; } /** * Read data from a source identified by an {@link URI}. * * @param uri URI for data source. * * @return File contents in a byte array. * * @throws IOException if the data could not be retrieved. */ public static byte[] readData( @NotNull final URI uri ) throws IOException { if ( LOG.isDebugEnabled() ) { LOG.debug( "readData( " + TextTools.quote( uri ) + " )" ); } final byte[] result; final String scheme = uri.getScheme(); if ( scheme == null ) { throw new IllegalArgumentException( "URI '" + uri + "' is invalid" ); } if ( "file".equals( scheme ) ) { final File file = new File( uri ); if ( !file.isFile() || !file.canRead() ) { throw new FileNotFoundException( uri.toString() ); } final long fileSize = file.length(); try { final InputStream in = new FileInputStream( file ); result = new byte[ (int)fileSize ]; try { int done = 0; while ( done < result.length ) { done += in.read( result, done, result.length - done ); } } finally { in.close(); } } catch ( SecurityException e ) { throw new IOException( String.valueOf( e ) ); } } else if ( "ftp".equals( scheme ) ) { final URIPath path = new URIPath( uri ); final Map parameters = path.getParameters(); String directory = path.getDirectoryWithoutSlash(); String filename = path.getFile(); if ( TextTools.isEmpty( filename ) ) { throw new IOException( "Bad path in URI: " + uri ); } if ( parameters.containsKey( "no-chdir" ) ) { filename = path.getDirectory() + filename; directory = ""; } final FTPClient ftpClient = openFtpConnection( uri ); try { final String type = parameters.get( "type" ); if ( type != null ) { if ( "a".equals( type ) ) { ftpClient.setFileType( FTP.ASCII_FILE_TYPE ); } else if ( "i".equals( type ) ) { ftpClient.setFileType( FTP.BINARY_FILE_TYPE ); } } if ( directory.length() > 0 ) { if ( !ftpClient.changeWorkingDirectory( directory ) ) { throw new FileNotFoundException( "Failed to access directory for URI: " + uri + " (" + ftpClient.getReplyCode() + ": " + ftpClient.getReplyString() + ')' ); } } final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); if ( !ftpClient.retrieveFile( filename, byteOut ) ) { throw new IOException( "Failed to retrieve file for URI (reply code " + ftpClient.getReplyCode() + ": " + ftpClient.getReplyString() + "): " + uri ); } result = byteOut.toByteArray(); } finally { if ( ftpClient.isConnected() ) { ftpClient.disconnect(); } } } else if ( "memory".equals( scheme ) ) { result = MemoryScheme.getData( uri ); } else if ( "smb".equals( scheme ) ) { final SmbFile file = new SmbFile( "smb:" + uri.getSchemeSpecificPart() ); try { if ( !file.isFile() ) { throw new FileNotFoundException( uri.toString() ); } } catch ( SmbException e ) { throw new IOException( uri + " => " + e.getMessage(), e ); } catch ( ExceptionInInitializerError e ) { throw new IOException( uri + " => " + e.getMessage(), e ); } final long fileSize = file.length(); final InputStream in = file.getInputStream(); result = new byte[ (int)fileSize ]; try { int done = 0; while ( done < result.length ) { final int read = in.read( result, done, result.length - done ); if ( read < 0 ) { throw new EOFException( uri.toString() ); } done += read; } } finally { in.close(); } } else { final URL url = uri.toURL(); final URLConnection connection = url.openConnection(); connection.setDoInput( true ); connection.setDoOutput( false ); final InputStream in = connection.getInputStream(); try { result = DataStreamTools.readByteArray( in ); } finally { in.close(); } } return result; } /** * Write data to a destination identified by an {@link URI}. * * @param uri URI for data destination. * @param data Data to write. * @param append Append to existing data vs. create new data.. * * @throws IOException if the data could not be written. */ public static void writeData( @NotNull final URI uri, @NotNull final byte[] data, final boolean append ) throws IOException { if ( LOG.isDebugEnabled() ) { LOG.debug( "writeData( " + TextTools.quote( uri ) + ", data[" + data.length + "], append=" + append + " )" ); } final String scheme = uri.getScheme(); if ( scheme == null ) { throw new IllegalArgumentException( "URI '" + uri + "' is invalid" ); } try { if ( "file".equals( scheme ) ) { final File file = new File( uri ); try { final File parent = file.getParentFile(); if ( parent != null ) { parent.mkdirs(); } final OutputStream out = new FileOutputStream( file, append ); try { out.write( data ); } finally { out.close(); } } catch ( SecurityException e ) { throw new IOException( String.valueOf( e ) ); } } else if ( "ftp".equals( scheme ) ) { final URIPath path = new URIPath( uri ); final Map parameters = path.getParameters(); String directory = path.getDirectoryWithoutSlash(); String filename = path.getFile(); if ( TextTools.isEmpty( filename ) ) { throw new IOException( "Bad path in URI: " + uri ); } if ( parameters.containsKey( "no-chdir" ) ) { filename = path.getDirectory() + filename; directory = ""; } final FTPClient ftpClient = openFtpConnection( uri ); try { if ( directory.length() > 0 ) { if ( LOG.isTraceEnabled() ) { LOG.trace( "changeWorkingDirectory( " + directory + " )" ); } if ( !ftpClient.changeWorkingDirectory( directory ) ) { if ( LOG.isDebugEnabled() ) { LOG.debug( "makeDirectory( " + directory + " )" ); } if ( !ftpClient.makeDirectory( directory ) ) { throw new IOException( "Failed to make directory for URI (reply code " + ftpClient.getReplyCode() + ": " + ftpClient.getReplyString() + "): " + uri ); } if ( LOG.isTraceEnabled() ) { LOG.trace( "changeWorkingDirectory( " + directory + " )" ); } if ( !ftpClient.changeWorkingDirectory( directory ) ) { throw new FileNotFoundException( "Failed to enter newly created directory for URI (reply code " + ftpClient.getReplyCode() + ": " + ftpClient.getReplyString() + "): " + uri ); } } } if ( parameters.containsKey( "delete" ) ) { if ( LOG.isTraceEnabled() ) { LOG.trace( "deleteFile( '" + filename + "' )" ); } try { ftpClient.deleteFile( filename ); } catch ( IOException e ) { if ( LOG.isDebugEnabled() ) { LOG.debug( "deleteFile( " + filename + " ) => " + e ); } throw e; } } if ( LOG.isTraceEnabled() ) { LOG.trace( "storeFile( '" + filename + "' )" ); } if ( !ftpClient.storeFile( filename, new ByteArrayInputStream( data ) ) ) { throw new IOException( "Failed to store file for URI (reply code " + ftpClient.getReplyCode() + ": " + ftpClient.getReplyString() + "): " + uri ); } ftpClient.quit(); } finally { if ( ftpClient.isConnected() ) { ftpClient.disconnect(); } } } else if ( "memory".equals( scheme ) ) { final OutputStream out = MemoryScheme.getOutputStream( uri, append ); out.write( data ); } else if ( "serial".equals( scheme ) ) { final Javacomm javacomm = Javacomm.getInstance(); javacomm.sendToSerialPort( uri, data ); } else if ( "smb".equals( scheme ) ) { final SmbFile file = new SmbFile( "smb:" + uri.getSchemeSpecificPart() ); try { final SmbFile parent = new SmbFile( file.getParent(), (NtlmPasswordAuthentication)file.getPrincipal() ); if ( !parent.exists() ) { parent.mkdirs(); } } catch ( MalformedURLException e ) { /* * We couldn't get a parent, so we don't need to create one, * and can safely ignore this exception. */ } catch ( SmbException e ) { /* * We may not have been able to access the parent here, or * the parent directory/ies could not be created. * * We ignore these situations, since either may be caused by * various non-fatal problems. */ } final OutputStream out = new SmbFileOutputStream( file, append ); try { out.write( data ); } finally { out.close(); } } else if ( "socket".equals( scheme ) ) { final Socket socket = new Socket(); try { socket.connect( new InetSocketAddress( uri.getHost(), uri.getPort() ), 10000 ); final OutputStream out = socket.getOutputStream(); out.write( data ); } finally { socket.close(); } } else { if ( LOG.isTraceEnabled() ) { LOG.debug( "falling back to default protocol handler" ); } final URL url = uri.toURL(); final URLConnection connection = url.openConnection(); connection.setDoInput( false ); connection.setDoOutput( true ); final OutputStream out = connection.getOutputStream(); try { out.write( data ); } finally { out.close(); } } } catch ( SecurityException e ) { throw new IOException( String.valueOf( e ) ); } } /** * Open an FTP connection to the server specified in the given URI. * * @param uri FTP URI. * * @return {@link FTPClient} with open FTP connection. * * @throws IOException if it was not possible to connect through FTP. */ public static FTPClient openFtpConnection( final URI uri ) throws IOException { /* * Get FTP connection properties. */ final String user; final String password; final String userInfo = uri.getUserInfo(); if ( userInfo != null ) { final int colon = userInfo.indexOf( (int)':' ); if ( colon >= 0 ) { user = userInfo.substring( 0, colon ); password = userInfo.substring( colon + 1 ); } else { user = userInfo; password = ""; } } else { user = "anonymous"; password = ""; } final String hostname = uri.getHost(); if ( TextTools.isEmpty( hostname ) ) { throw new IOException( "Missing hostname in URI: " + uri ); } final int port = ( uri.getPort() >= 0 ) ? uri.getPort() : 21; /* * Setup FTP session. */ final FTPClient result = new FTPClient(); boolean success = false; try { final URIPath path = new URIPath( uri ); final Map parameters = path.getParameters(); final String connectMode = parameters.get( "mode" ); result.setConnectTimeout( 10000 ); result.connect( hostname, port ); result.setSoTimeout( 10000 ); result.setDataTimeout( 10000 ); result.login( user, password ); if ( "active".equals( connectMode ) ) { result.enterLocalActiveMode(); } else //if ( "passive".equals( connectMode ) ) { result.enterLocalPassiveMode(); } result.setFileType( FTP.BINARY_FILE_TYPE ); if ( LOG.isTraceEnabled() ) { result.addProtocolCommandListener( new ProtocolCommandListener() { @Override public void protocolCommandSent( @NotNull final ProtocolCommandEvent event ) { LOG.trace( uri + ": client: " + event.getMessage() ); } @Override public void protocolReplyReceived( @NotNull final ProtocolCommandEvent event ) { LOG.trace( uri + ": server: " + event.getMessage() ); } } ); } success = true; } finally { if ( !success && result.isConnected() ) { try { result.quit(); } catch ( Throwable t ) { /* we don't care if we already have a problem */ } try { result.disconnect(); } catch ( Throwable t ) { /* we don't care if we already have a problem */ } } } return result; } /** * Opens a connection to the specified resource. * * @param uri Resource to connect to. * * @return Connection for the given URI. */ public static URIConnection openConnection( final URI uri ) { // TODO: Provide more efficient implementations for specific schemes. return new BasicURIConnection( uri ); } /** * Utility/Application class is not supposed to be instantiated. */ private URITools() { } /** * Represents the path component of a Uniform Resource Identifier (URI), as * specified by RFC 3986: URI * Generic Syntax. Path segments can be further parsed to simplify * interpretation of common Internet schemes defined in RFC 1738: Uniform Resource * Locators (URL). */ public static class URIPath { /** * Whether the path is absolute. */ private boolean _absolute; /** * Whether the path is a directory. */ private boolean _directory; /** * Path segments without scheme-specific parameters and * percent-decoded. */ private List _segments; /** * Scheme-specific parameters, extracted from all path segments. */ private Map _parameters; /** * Constructs a new URI path. * * @param uri URI to get the path from. * * @throws IllegalArgumentException if the given URI is opaque. */ public URIPath( @NotNull final URI uri ) { this( uri.getRawPath() /* null for opaque URLs */ ); } /** * Constructs a new URI path. * * @param rawPath URI path, percent-encoded. */ public URIPath( @NotNull final String rawPath ) { _absolute = TextTools.startsWith( rawPath, '/' ); _directory = TextTools.endsWith( rawPath, '/' ); final List segments; Map parameters = null; if ( TextTools.isEmpty( rawPath ) ) { segments = Collections.emptyList(); } else { final String[] rawSplit = rawPath.split( "/" ); segments = new ArrayList( rawSplit.length ); for ( int i = 0; i < rawSplit.length; i++ ) { final String segment = rawSplit[ i ]; if ( ( i != 0 ) || !segment.isEmpty() ) { final String[] pathAndParameters = segment.split( ";" ); if ( pathAndParameters.length > 0 ) { segments.add( percentDecode( pathAndParameters[ 0 ] ) ); if ( pathAndParameters.length > 1 ) { if ( parameters == null ) { parameters = new LinkedHashMap(); } for ( int j = 1; j < pathAndParameters.length; j++ ) { final String entry = pathAndParameters[ j ]; final int equals = entry.indexOf( '=' ); final String key = ( equals == -1 ) ? entry : entry.substring( 0, equals ); final String value = ( equals == -1 ) ? null : percentDecode( entry.substring( equals + 1 ) ); parameters.put( percentDecode( key ), value ); } } } } } } _segments = segments; _parameters = ( parameters == null ) ? Collections.emptyMap() : parameters; } /** * Returns the path as a list of percent-decoded path segments, with * parameters stripped from each path segment. * * @return Path segments, not including any parameters. */ public List getSegments() { return Collections.unmodifiableList( _segments ); } /** * Parses parameters specified in path segments, as used by common * Internet schemas such as FTP and HTTP. The following syntax is used: * *
		 * segment = name ( ";" key ( "=" value )? )*
		 * 
* * @return Parameters for all path segments. */ public Map getParameters() { return Collections.unmodifiableMap( _parameters ); } /** * Returns whether the path is absolute. Absolute paths start with a * slash. * * Note that a URI with authority and an empty path is defined to be * absolute, but since the path is empty (and contains no slash), this * method will return {@code false}. * * @return {@code true} if the path is absolute. */ public boolean isAbsolute() { return _absolute; } /** * Returns whether the path denotes a directory. A path is understood to * denote a directory if and only if it ends with a slash. * * @return {@code true} if the path denotes a directory. */ public boolean isDirectory() { return _directory; } /** * Returns the directory part of the path, i.e. the path up to and * including the last slash. Any parameters that the path may contain * are not included in the result. * * @return Directory name. */ @NotNull public String getDirectory() { final StringBuilder result = new StringBuilder(); if ( isEmpty() ) { if ( isDirectory() ) { result.append( '/' ); } } else { if ( isAbsolute() ) { result.append( '/' ); } final List segments = getSegments(); final int segmentCount = isDirectory() ? segments.size() : segments.size() - 1; for ( int i = 0; i < segmentCount; i++ ) { final String segment = segments.get( i ); result.append( segment ); result.append( '/' ); } } return result.toString(); } /** * Returns the directory part of the path, i.e. the path up to and * excluding the last slash. Any parameters that the path may contain * are not included in the result. * * @return Directory name. */ @NotNull public String getDirectoryWithoutSlash() { final StringBuilder result = new StringBuilder(); if ( isAbsolute() ) { result.append( '/' ); } if ( !isEmpty() ) { final List segments = getSegments(); final int segmentCount = isDirectory() ? segments.size() : segments.size() - 1; for ( int i = 0; i < segmentCount; i++ ) { if ( i > 0 ) { result.append( '/' ); } final String segment = segments.get( i ); result.append( segment ); } } return result.toString(); } /** * Returns the file name part of the path. Any parameters that the path * may contain are not included in the result. * * @return File name; empty for a path denoting a directory. */ public String getFile() { final String result; if ( isDirectory() || isEmpty() ) { result = ""; } else { final List segments = getSegments(); result = segments.get( segments.size() - 1 ); } return result; } /** * Returns whether the URL is empty. * * @return {@code true} if the URL is empty. */ private boolean isEmpty() { return _segments.isEmpty(); } @Override public String toString() { final StringBuilder result = new StringBuilder(); if ( isAbsolute() ) { result.append( '/' ); } final List segments = getSegments(); final Map parameters = getParameters(); boolean noColon = !isAbsolute(); try { for ( final Iterator i = segments.iterator(); i.hasNext(); ) { percentEncode( result, i.next(), noColon ? PATH_DELIMS : PATH_DELIMS_COLON ); if ( i.hasNext() ) { result.append( '/' ); } else { for ( final Map.Entry entry : parameters.entrySet() ) { result.append( ';' ); percentEncode( result, entry.getKey() ); if ( entry.getValue() != null ) { result.append( '=' ); percentEncode( result, entry.getValue() ); } } } noColon = false; } } catch ( IOException e ) { // Never thrown by string builder. throw new AssertionError( e ); } if ( isDirectory() ) { if ( !segments.isEmpty() || !isAbsolute() ) { result.append( '/' ); } } return result.toString(); } } /** * RFC 3986 grammar: characters allowed in 'segment-nz-nc' production rule, * excluding the semi-colon, which delimits the segment's parameters. */ private static final char[] PATH_DELIMS = { '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', '=' }; /** * RFC 3986 grammar: characters allowed in the 'pchar' production rule, * excluding the semi-colon, which delimits the segment's parameters. */ private static final char[] PATH_DELIMS_COLON = { '@', ':', '!', '$', '&', '\'', '(', ')', '*', '+', ',', '=' }; /** * Returns whether the given character is unreserved. * * @param c Character. * * @return {@code true} for unreserved characters. */ private static boolean isUnreserved( final char c ) { return ( c >= 'A' ) && ( c <= 'Z' ) || ( c >= 'a' ) && ( c <= 'z' ) || ( c >= '0' ) && ( c <= '9' ) || ( c == '-' ) || ( c == '.' ) || ( c == '_' ) || ( c == '~' ); } /** * Percent-encodes the given string and appends the result to the given * appendable. * * @param out Character sequence to append the result to. * @param s String to be encoded. * @param allowed Characters that, in addition to unreserved characters, * must not be encoded. * * @throws IOException if an I/O error occurs. */ private static void percentEncode( final Appendable out, final String s, final char... allowed ) throws IOException { final char[] pair = new char[ 2 ]; for ( int i = 0; i < s.length(); i = s.offsetByCodePoints( i, 1 ) ) { final int codePoint = s.codePointAt( i ); if ( Character.toChars( codePoint, pair, 0 ) == 1 ) { final char c = pair[ 0 ]; if ( isUnreserved( c ) ) { out.append( c ); } else { boolean encode = true; for ( final char allowedChar : allowed ) { if ( c == allowedChar ) { encode = false; break; } } if ( encode ) { final String str = String.valueOf( c ); for ( final byte b : str.getBytes( "UTF-8" ) ) { appendPercentEncodedByte( out, b ); } } else { out.append( c ); } } } else { final String str = String.valueOf( pair ); for ( final byte b : str.getBytes( "UTF-8" ) ) { appendPercentEncodedByte( out, b ); } } } } /** * @param out Character sequence to append the result to. * @param b Byte to be encoded. * * @throws IOException if an I/O error occurs. */ private static void appendPercentEncodedByte( final Appendable out, final byte b ) throws IOException { final char toUpper = (char)( (int)'A' - (int)'a' ); out.append( '%' ); { char c = Character.forDigit( ( b >> 4 ) & 0xf, 16 ); if ( c >= 'a' ) { c += toUpper; } out.append( c ); } { char c = Character.forDigit( (int)b & 0xf, 16 ); if ( c >= 'a' ) { c += toUpper; } out.append( c ); } } /** * Percent-decodes the given string. * * @param s String to be decoded. * * @return Decoded string. */ private static String percentDecode( final String s ) { try { return URLDecoder.decode( s, "UTF-8" ); } catch ( UnsupportedEncodingException e ) { // Never thrown for UTF-8 encoding. throw new AssertionError( e ); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy