/* Copyright (c) 2011-2022 Reliancy LLC Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3. You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html. You may not use this file except in compliance with the License. */ package com.reliancy.util; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; /** Path to a resource almost identical to a URL. * It might but should not hold any handles. It holds the address and * possibly takes care of looking up addresses. * The richest syntax is: *
 {@code PROTOCOL://USER:PWD@MACHINE:PORT/DATABASE?key=val&... } 
* Properties are held in their own string and need to be decoded. * * We use forward slash for path delimitation of database portion. For the rest we preserve other slashes to allow windows domain\\user or server\\instance. * Special chars are [/@?,:;] * if any are found at the end of protocol we skip :// * if any are found at the beginning of properties we skip ? * if any are found at the beginning of database we skip / * * In windows we have volume or drive letters which are postfixed by colon : then slashes. We treat the volume as part of database. * We store the database without first slash to allow specification of relative paths. * To render it as absolute set protocol or host to empty string instead of null. Then a slash will be prefixed and you will get absolute path. * @author amer */ public class Path { static final String SYMBOLS="/@?,:;"; String connectstring; String protocol; ///< protocol guides the interpretation of the other elements in conn, string String userid; ///< authentication String password; ///< authorization String host; ///< machine or computer String port; ///< access to computer String database; ///< name of database or filename String properties; ///< properties are what follows ? in a url public Path(String connect) { setConnectString(connect); } public Path(Path in) { connectstring=in.connectstring; protocol=in.protocol; userid=in.userid; password=in.password; host=in.host; port=in.port; database=in.database; properties=in.properties; } @Override public String toString() { return getConnectString(); } /** * Converts the path to a file. * If absolute is true * @param absolute if true forms absolute path else will return relative path * @return */ public File toFile(boolean absolute){ String proto=getProtocol(); String host=getHost(); try{ setProtocol(absolute?"":null); setHost(absolute?"":null); String path=toString(); return new File(path); }finally{ setProtocol(proto); setHost(host); } } public URL toURL() throws MalformedURLException{ String path=toString(); if(Handy.isBlank(getHost()) && path.contains("://") && !path.contains(":///")) path=path.replace("://",":///"); return new URL(path); } public void clear(){ connectstring=null; protocol=null; userid=null; password=null; host=null; port=null; database=null; properties=null; } public String getConnectString() { if(connectstring!=null) return connectstring; // assemble the connect string StringBuilder buf=new StringBuilder(); //boolean absolute=false; if(!Handy.isBlank(protocol)){ buf.append(protocol); if(SYMBOLS.indexOf(protocol.charAt(protocol.length()-1))<0) buf.append("://"); } if(!Handy.isBlank(host)){ if(userid!=null && password!=null){ buf.append(userid).append(":").append(password).append("@"); } buf.append(host); if(port!=null) buf.append(":").append(port); } if(!Handy.isBlank(database)){ if(buf.length()>0 && SYMBOLS.indexOf(database.charAt(0))<0){ // we got something in front so we need to use slash buf.append("/"); }else if(protocol!=null || host!=null){ boolean winvol=database.length()>2 && database.charAt(1)==':' && (database.charAt(2)=='/' || database.charAt(2)=='\\'); // we got nothing in front but if host or protocol empty but not null we treat as absolute if(!winvol) buf.append("/"); } buf.append(database); } if(properties!=null){ if(SYMBOLS.indexOf(properties.charAt(0))<0) buf.append("?"); buf.append(properties); } connectstring=buf.toString(); return connectstring; } public void setConnectString(String connect) { clear(); if (connect == null) { return; } this.connectstring=connect; // first get protocol - everything up to : which is not followed by a symbol (includes :// but also c:/ int oldst=0; int st=0; for(int i=0;i<(connectstring.length()-1);i++){ char curr=connectstring.charAt(i); if(curr==':'){ oldst=st; st=i; }else if(SYMBOLS.indexOf(curr)!=-1){ if(curr=='@') st=oldst; // this will back out one : if protocl search ended with @ indicating a server break; } } if(st==1){ st=0;} // this will supress single letter protocols i.e. c:/ ued in windows as part of database/file if(2==(st-oldst)) st=oldst; if(st>0){ this.protocol=connectstring.substring(0,st); while(SYMBOLS.indexOf(connectstring.charAt(st))!=-1) st++; // advance over symbols } // next assume the rest is a file/database database = connectstring.substring(st); // now check for user id and password st = database.indexOf('@'); boolean checkhost=st>=0; if(!Handy.isBlank(protocol)){ checkhost=!protocol.contains(":file") && !protocol.contains(":mem") && !protocol.equals("file") && !protocol.equals("mem");; } if (st != -1) { userid = database.substring(0, st); if(userid.contains("%4")) try{userid=URLDecoder.decode(userid,"UTF-8");}catch(Exception e){} database = database.substring(st + 1); // now try to split user id into password if possible st = userid.indexOf(':'); if (st != -1) { password = userid.substring(st + 1); userid = userid.substring(0, st); } } // ok next try to split up machine if possible (only for absolute urls) st = database.indexOf(':'); if(st<0) st=database.indexOf('/'); if (st != -1 && checkhost) { boolean portfollows=database.charAt(st)==':'; host = database.substring(0, st); // now try to recover port if(portfollows){ int st2 = database.indexOf(':',st+1); if(st2<0) st2 = database.indexOf('/',st+1); if (st2 != -1 && st2>(st+1)) { // we have a port port = database.substring(st + 1,st2); st=st2; }else{ // no port we have : then / - which is used in windows to indicate volume and we treat as part of database st=-1; host=""; } } database = database.substring(st + 1); } database=fixSlashes(database); // finally split the properties from database st = database.indexOf('?'); if(st==-1) st=database.indexOf(';'); if (st != -1) { // we have properties properties = database.substring(st); database = database.substring(0, st); } } /** * Absolute ResourcePath will have a protocol */ public boolean isAbsolute() { return (protocol != null || host!=null); } /// will clear host and protocol using empty string thereby making database absolute path public Path setAbsolute(){ setHost(""); setProtocol(""); return this; } public String getDatabase() { return database; } public Path setDatabase(String database) { this.database = database; connectstring=null; return this; } public String getHost() { return host; } public Path setHost(String host) { this.host = host; connectstring=null; return this; } public String getPassword() { return password; } public Path setPassword(String password) { this.password = password; connectstring=null; return this; } public String getPort() { return port; } public Path setPort(String port) { this.port = port; connectstring=null; return this; } public String getProtocol() { return protocol; } public Path setProtocol(String protocol) { this.protocol = protocol; connectstring=null; return this; } public String getUserid() { return userid; } public Path setUserid(String userid) { this.userid = userid; connectstring=null; return this; } public String getProperties() { return properties; } public Path setProperties(String userid) { this.properties = userid; connectstring=null; return this; } public String getBase() { return Path.getBase(database); } public String getExtension() { return Path.getExtension(database); } public String getPathItem() { return Path.getPathItem(database); } /** Ensures that we use forward slashes and that single dot is not present mid or and the end. * * @param path a unix or windows or uri path * @return a path with forward slashes */ public static String fixSlashes(String path) { if(path==null || path.length()==0) return path; path=path.replace("\\", "/"); path=path.replace("/./","/"); while(true){ if(path.endsWith("/")) path=path.substring(0,path.length()-1); else if(path.endsWith("/.")) path=path.substring(0,path.length()-2); else break; } return path; } /** returns database path given path and file. * We assume the path uses forward backslash for delimitation. */ public static String getBase(String path) { int st1 = path.lastIndexOf('/'); int st2 = path.lastIndexOf('\\'); int st=st2>st1?st2:st1; if (st == -1) { return null; } return path.substring(0, st); } public static String getExtension(String path) { int st=Math.max(path.lastIndexOf('/'),path.lastIndexOf('\\')); int st2 = path.lastIndexOf('.'); if (st2 == -1 || (st>0 && st2=url.length()) return null; return url.substring(base.length()); } /** * unites two paths. * @param base * @param url */ public static String getUnion(String base,String url){ if(base==null || base.isEmpty()){ return url; } if(url==null || url.isEmpty()){ return base; } //if(url.startsWith(base)) return url; if(Handy.indexOf(url,base,0)==0) return url; StringBuilder ret=new StringBuilder(); ret.append(base); if(!base.endsWith("/") && !url.startsWith("/")) ret.append("/"); if(base.endsWith("/") && url.startsWith("/")) ret.setLength(ret.length()-1); ret.append(url); return ret.toString(); } /** * method will split paths used in linux and windows. * in particular for windows it checks if a single letter precedes a colon in which case it considers it a volume * and does not split there. * @param _paths paths joined with colon or semi-colon * @return array of paths */ public static String[] splitPaths(String _paths){ String[] paths=_paths.replaceAll("(;|:|^)([a-zA-Z]):","$1$2##").split("[:;]"); for (int i = 0; i < paths.length; i++) { String path=paths[i]; path = path.replace("##",":"); path=path.replace("/./","/"); path=path.replace("//","/"); path=path.replace("\\.\\","\\"); path=path.replace("\\\\","\\"); paths[i]=path; } return paths; } /** * Returns a list of key,value pairs in the order they occur in the string str. * @param str */ public static String[] splitProperties(String str) { if(str.startsWith("?")) str=str.substring(1); if(str.startsWith(";")) str=str.substring(1); return str.split("&"); } public static String[] splitKeyValue(String str) { String[] t=str.split("="); if(t==null || t.length==0) return null; t[0]=Handy.trim(t[0],"'\""); return t; } public static String[] split(String str) { return str.split("/"); } }