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

cat.inspiracio.url.URL Maven / Gradle / Ivy

The newest version!
/*
Copyright 2015 Alexander Bunkenburg 

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cat.inspiracio.url;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** A proper java bean for a URL of the http protocol. 
 * 
 * Has getters and setters for Java bean style.
 * Also has methods for fluid style.
 * 
 * Intended to implement https://url.spec.whatwg.org/.
 * */
public class URL implements Serializable, URLUtils {

	private static final long serialVersionUID = -2542102834410732299L;

	// constants --------------------------
	
	/** The MIME types guessed from the extension. */
	private static final Maptypes=new HashMap();static{
		types.put("css", "text/css");
		types.put("csv", "text/csv");
		types.put("gif", "image/gif");
		types.put("html", "text/html");
		types.put("jpeg", "image/jpeg");
		types.put("jpg", "image/jpeg");
		types.put("js", "application/javascript");
		types.put("png", "image/png");
		types.put("txt", "text/plain");		
		types.put("xml", "application/xml");		
	}
	
	// state ------------------------------

	/** A URL’s scheme is an ASCII string that identifies the type of URL and 
	 * can be used to dispatch a URL for further processing after parsing. 
	 * 
	 * [spec: It is initially the empty string.] */
	private String scheme=null;

	/** A URL’s username is an ASCII string identifying a user. 
	 * [spec: It is initially the empty string.] */
	private String username=null;
	
	/** A URL’s password is either null or an ASCII string identifying a user’s 
	 * credentials. It is initially null.*/
	private String password=null;
	
	/** A URL’s host is either null or a host. It is initially null. 
	 * A host is a domain, an IPv4, or an IPv6. Not implemented: IPv4 and [IPv6]. */
	private String host=null;
	
	/** A URL’s port is either null or a 16-bit integer that identifies a 
	 * networking port. It is initially null. */
	private int port=0;//int is 32 bit. short is 16 bit. But I'll use int anyway.
	
	/** A URL’s path is a list of zero or more ASCII string holding data, usually 
	 * identifying a location in hierarchical form. It is initially the empty list. */
	private String path=null;//The concatenation, with initial '/'.
	
	/** A URL’s query is either null or an ASCII string holding data. It is initially null. */
	private URLParameters query=null;//null means no parameters
	
	/** A URL’s fragment is either null or a string holding data that can be used 
	 * for further processing on the resource the URL’s other components identify. 
	 * It is initially null. */
	private String fragment=null;
	
	/** A URL also has an associated non-relative flag. It is initially unset. */
	private boolean nonRelative=false;
	
	/** A URL also has an associated object that is either null or a Blob object. 
	 * It is initially null. Not implemented. */
	//private Object object;
	
	// construction -----------------------
	
	/** Extracts the URL from a request. 
	 * 
	 * Does not parse fragment, which is not sent to the server. */
//Very useful, but introduces dependency on javax.servlet.http.HttpServletRequest.
//	public URL(HttpServletRequest request){
//		scheme=request.getScheme();
//		server=request.getServerName();
//		port=request.getServerPort();
//		path=request.getRequestURI();
//		parameters=URLParameters.from(request);
//		fragment=null;//Not sent to the server.
//	}

	/** Makes a new URL, initially empty. */
	public URL(){}

	/** @param url Parses a URL from this string.
	 * Null is like empty string. */
	public URL(String url){
	    URLParser p=new URLParser();
        p.basic(url, null, null, this, null);
	}

	/** http://www.w3.org/TR/url/#dom-url
     *  Not implemented.
	 *  @param  url ...
	 *  @param base ... */
	public URL(String url, String base){throw new UnsupportedOperationException();}

	/** Deep clone of the URL. */
	@Override public URL clone(){
		URL clone=new URL();
		clone.nonRelative=nonRelative;
		clone.scheme=scheme;
		clone.username=username;
		clone.password=password;
		clone.host=host;
		clone.port=port;
		clone.path=path;
		clone.query=clone(query);
		clone.fragment=fragment;
		return clone;
	}

	// accessors ---------------------------
	
	public String getScheme(){return scheme;}
	public void setScheme(String s){scheme=s;}
	public String scheme(){return scheme;}
	public URL scheme(String s){scheme=s;return this;}
	
    private boolean isSpecialScheme(String scheme){
        if(scheme==null)return false;
        switch(scheme){
        case"ftp":
        case"file":
        case"gopher":
        case"http":
        case"https":
        case"ws":
        case"wss":return true;
        }
        return false;
    }
    boolean isSpecial(){return isSpecialScheme(scheme);}
    
    private boolean isLocalScheme(String scheme){
        if(scheme==null)return false;
        switch(scheme){
        case"about":
        case"blob":
        case"data":
        case"filesystem":return true;//Not "file"
        }
        return false;
    }
    boolean isLocal(){return isLocalScheme(scheme);}
    
    /** https://url.spec.whatwg.org/#syntax-url-absolute */
    boolean isAbsolute(){
        //simplifying: has a scheme
        return scheme!=null && !scheme.isEmpty();
    }
    
    /** https://url.spec.whatwg.org/#syntax-url-relative */
    boolean isRelative(){
        if(!isFile()){
            //no scheme
            if(scheme==null || scheme.isEmpty())
                return true;
            //no host
            if(host==null || host.isEmpty())
                return true;
            return false;
        }
        //"file"
        else{
            //no host
            return host==null || host.isEmpty();
        }
    }
    
    /** Is the scheme "file", case-insensitive? */
    private boolean isFile(){return "file".equalsIgnoreCase(scheme);}
    
	public void setProtocol(String p){setScheme(p);}
	public URL protocol(String p){setProtocol(p);return this;}
	
	/** Like "http:", with ":" at the end.
	 * If there is no scheme, returns ":".
	 * http://www.w3.org/TR/url/#dom-urlutils-protocol */
	public String getProtocol(){
	    if(scheme==null)return ":";
	    return scheme + ":";
	}
	public String protocol(){return getProtocol();}

	public void setUsername(String u){
	    //To set the username given a url and username, run these steps:
	    //1. Set url’s username to the empty string.
	    //2. For each code point in username, utf-8 percent encode it using the 
	    //userinfo encode set, and append the result to url’s username.
	    //The encoding is done in toString().
	    username=u;
	}
	public URL username(String u){username=u;return this;}
    public String getUsername(){return username;}
    public String username(){return username;}
    
    public void setPassword(String p){
        //To set the password given a url and password, run these steps:
        //1. If password is the empty string, set url’s password to null.
        if(p!=null && p.isEmpty())
            password=null;
        //2. Otherwise, run these substeps:
        else{
            //1. Set url’s password to the empty string.
            //2. For each code point in password, utf-8 percent encode it 
            //using the userinfo encode set, and append the result to url’s password.
        }
	    //The encoding is done in toString().
        password=p;
    }
    public URL password(String p){password=p;return this;}
    public String getPassword(){return password;}
    public String password(){return password;}
    
	public String getServer(){return host;}
	public void setServer(String s){host=s;}
	public String server(){return host;}
	public URL server(String s){host=s;return this;}
	
	public void setHostname(String n){setServer(n);}
	public String getHostname(){return host;}
	public URL hostname(String n){setServer(n);return this;}
	public String hostname(){return host;}
	
	/** Gets the string representing the port that was put explicitly in the URL, or "". */
	public String getPort(){
	    if(port==0)return "";
	    return Integer.toString(port);
	}
	
	public void setPort(int p){port=p;}
	public void setPort(String s){port=Integer.parseInt(s);}
	
	/** Gets the port, as int. 
	 * 
	 * If the port has not been set explicitly, returns the 
	 * default port for the scheme. 
	 * 
	 * If there is no scheme or the scheme has no default port,
	 * returns 0 as bad value. */
	public int port(){
	    if(port==0)
	        return getDefaultPort(scheme);//or 0.
	    return port;
	}
	public URL port(int p){port=p;return this;}
	
	/** Does this URL have the default port?
	 * If there is a scheme and a port, answer according to scheme.
	 * If there is no scheme and no port, true.
	 * Otherwise false. */
	boolean isDefaultPort(){
	    if(scheme!=null){
	        //We have scheme and explicit port.
	        if(port!=0)
	            return port==getDefaultPort(scheme);

	        //We have scheme but no explicit port: implicitly, use default port.
	        return true;
	    }
	    
	    //no scheme, no port: implicitly, use the standard port
	    if(port==0)return true;
	    
	    //No scheme, but have port: that's definitely not the default!
	    return false;
	}
	
	/** What's the default port for a scheme?
	 * Returns 0 if there is none. */
	static int getDefaultPort(String scheme){
	    if(scheme==null)return 0;
	    switch(scheme){
	    case "ftp":return 21;
	    case "gopher":return 70;
	    case "http":return 80;
	    case "https":return 443;
	    case "ws":return 80;
	    case "wss":return 443;
	    default: return 0;
	    }
	}
	
	/** Sets server and maybe also port.
	 * partial implementation: no IPs */
	public void setHost(String h){
	    if(h==null || h.isEmpty())
	        return;
	    int i=h.lastIndexOf(':');
	    if(i<0){
	        host=h;
	        return;
	    }
	    if(0
	 * Not clear whether the resulting URL should end in "/" or not. 
	 * I'll leave the final "/".
	 * @return ... */
	public URL pop(){
        URL clone=clone();
	    if("file".equals(scheme) && isNormalizedWindowsDriveLetter(path) )
	        return clone;
	    
		String path=clone.path();
		if(path!=null){
			int i=path.lastIndexOf('/');
			
			//Path has "/".
			if(0<=i){
				path=path.substring(0, i+1);
				clone.path(path);
			}
			
			//Path has no "/".
			else
			    clone.path("");
		}
		//removing parameters and fragment is not in spec
		return clone.removeParameters().removeFragment();
	}
	
	/** @return Gets the extension, without leading "." or null if there is none. */
	public String getExtension(){
		if(path==null)return null;
		int i=path.lastIndexOf('.');
		if(i<0)return null;
		return path.substring(i+1);
	}
	public String extension(){return getExtension();}
	
	/** @param extension Like "html", without leading "." 
	 * If the extension is null, removes the exception. */
	public void setExtension(String extension){
		if(path==null)path="";//If the path is null, setExtension() makes no sense anyway.
		int i=path.lastIndexOf('.');
		if(0 getParameters(String key){return parameters(key);}
	public List parameters(String key){
		if(query==null)return Collections.emptyList();
		return query.getValues(key);
	}
	
	/** Adds a parameter.
	 * Setting a parameter to null removes it.
	 * @param key ...
	 * @param value ... */
	public void addParameter(String key, String value){
		if(value==null){
			removeParameter(key);
			return;
		}
		if(query==null)query=new URLParameters();
		query.add(key, value);
	}

	/** Sets a parameter.
	 * Overwrites the parameter if it is already set.
	 * Setting a parameter to null removes it.
	 * @param key ...
	 * @param value ... */
	public void setParameter(String key, String value){
		if(value==null)removeParameter(key);
		else parameter(key, value);
	}
	public URL parameter(String key, String value){
		if(query==null && value==null)return this;
		if(query==null)query=new URLParameters();
		query.set(key, value);
		return this;
	}

	/** @return May return empty parameters, but never null. */
	public URLParameters getParameters(){return parameters();}
	public URLParameters parameters(){
		if(query==null)query=new URLParameters();
		return query;
	}
	
	/** Sets all parameters. Passing null removes all parameters.
	 * @param ps ... */
	public void setParameters(URLParameters ps){parameters(ps);}
	
	/** Sets all parameters. Passing null removes all parameters.
	 * @param ps ...
	 * @return ... */
	public URL parameters(URLParameters ps){query=ps;return this;}

	public URL removeParameter(String key){parameter(key, null);return this;}
	public URL removeParameters(){query=null;return this;}
	
	public void setSearchParams(URLSearchParams ps){query=ps;}
	public URLSearchParams getSearchParams(){
	    if(query==null)return null;
	    if(query instanceof URLSearchParams)return (URLSearchParams)query;
	    query=new URLSearchParams(query);
	    return (URLSearchParams)query;
	}

	public void setSearch(String s){
	    if(null==s || 0==s.length()){
	        removeParameters();
	        return;
	    }
	    if(s.startsWith("?"))
	        s=s.substring(1);
	    query=new URLParameters(s);
	}
	
	/** Gets the query string, with initial "?", or null.
     * http://www.w3.org/TR/url/#dom-urlutils-search
     * @return Like "?key=value&key=value". */
	public String getSearch(){
	    if(query==null || query.empty())return "";
	    return "?" + query.toString();
	}
	
	/** Does the path end with this suffix?
	 * @param suffix ...
	 * @return ... */
	public boolean endsWith(String suffix){
		if(path==null)return suffix==null;
		return path.endsWith(suffix);
	}

	public String fragment(){return fragment;}
	public String getFragment(){return fragment();}
	public void setFragment(String f){fragment=f;}
	public URL fragment(String f){fragment=f;return this;}
	public URL removeFragment(){return fragment(null);}

	/** Sets the fragment. Setting it to empty string or null
	 * removes the fragment.
	 * @param h New fragment. If it doesn't start with "#", 
	 * 	the initial "#" is assumed. */
	public void setHash(String h){
	    if("javascript".equals(scheme))return;
	    if(h==null || "".equals(h)){
	        fragment=null;
	        return;
	    }
	    if(h.startsWith("#"))
	        h=h.substring(1);
	    fragment=h;
	}
	public URL hash(String h){setHash(h);return this;}
	
	/** Gets the fragment, with initial "#", or empty string. */
	public String getHash(){
	    if(fragment==null)return "";
	    return "#" + fragment;
	}
	public String hash(){return getHash();}
	
	/** The complete URL as String.
	 *  
	 * Username and password are shown, encoded.
	 * 
	 * If the port is standard, it will not appear.
	 * The parameters appear in the order they were inserted. 
	 **/
	@Override public String toString(){
		
		StringBuilder builder=new StringBuilder();
		
		if(scheme!=null && !scheme.isEmpty()){
			builder.append(scheme);
			builder.append(':');
		}
		
		if(host!=null && !host.isEmpty()){
			builder.append("//");
			if(username!=null && !username.isEmpty()){
			    builder.append(encode(username));
			    if(password!=null)
			        builder.append(':').append(encode(password));
			    builder.append('@');
			}
			builder.append(host);
		}
		
		//don't show port 0 and default ports
		if(port!=0 && !isDefaultPort()){
			builder.append(":");
			builder.append(port);
		}
		
		if(path!=null && !path.isEmpty()){
			//If we have scheme, host, or port, make sure path starts with "/".
			if(0
	 * 	
  • if there is no port, uses default port for the scheme *
  • ignores parameter order * * */ @Override public boolean equals(Object o){ if(o==null)return false; if(this==o)return true; if(!(o instanceof URL))return false; URL a=(URL)o; URL b=this; return equals(a.scheme, b.scheme) && equals(a.username, b.username) && equals(a.password, b.password) && equals(a.host, b.host) && a.port()==b.port() && equals(a.path, b.path) && equals(a.query, b.query) && equals(a.fragment, b.fragment); } // static API methods ------------------------------------ /** Not implemented * @param domain ... * @return ... */ public static String domainToASCII(String domain){throw new UnsupportedOperationException();} /** Not implemented * @param domain ... * @return ... */ public static String domainToUnicode(String domain){throw new UnsupportedOperationException();} // helpers ----------------------------------------------- /** Cloning parameters without stumbling over null. * @param ps ... * @return ... */ private URLParameters clone(URLParameters ps){ if(ps==null || ps.isEmpty()) return null; return ps.clone(); } /** Equality without stumbling over null. */ private boolean equals(Object a, Object b){ if(a==null)return b==null; return a.equals(b); } boolean isNonRelative(){return nonRelative;} void setNonRelative(boolean nonRelative){this.nonRelative=nonRelative;} private boolean isASCIIAlpha(char c){return ('A'<=c && c<='Z') || ('a'<=c && c<='z');} /** A normalized Windows drive letter is a Windows drive letter of which * the second code point is ":". */ private boolean isNormalizedWindowsDriveLetter(String s){ return isWindowsDriveLetter(s) && s.charAt(1)==':'; } /** A Windows drive letter is two code points, of which the first is an * ASCII alpha and the second is either ":" or "|".*/ private boolean isWindowsDriveLetter(String s){ if(s==null || 2!=s.length()) return false; return isWindowsDriveLetter(s.charAt(0), s.charAt(1)); } /** A Windows drive letter is two code points, of which the first is an * ASCII alpha and the second is either ":" or "|".*/ private boolean isWindowsDriveLetter(char a, char b){ return isASCIIAlpha(a) && (b==':' || b=='|'); } /** utf-8 percent encode codePoint using the user info encode set. * Spec: https://url.spec.whatwg.org/#utf-8-percent-encode */ private String encode(String s){ //The simple encode set are C0 controls and all code points greater than U+007E. //The default encode set is the simple encode set and code points U+0020, '"', "#", "<", ">", "?", "`", "{", and "}". //The user info encode set is the default encode set and code points "/", ":", ";", "=", "@", "[", "\", "]", "^", and "|". //approximate implementation try{ return URLEncoder.encode(s, "UTF-8");//UnsupportedEncodingException }catch(UnsupportedEncodingException e){throw new RuntimeException(e);} } /** utf-8 percent decode codePoint using the user info encode set. * Spec: https://url.spec.whatwg.org/#utf-8-percent-encode */ @SuppressWarnings("unused") private String decode(String s){ try{ return URLDecoder.decode(s, "UTF-8");//UnsupportedEncodingException }catch(UnsupportedEncodingException e){throw new RuntimeException(e);} } }




  • © 2015 - 2025 Weber Informatics LLC | Privacy Policy