gov.nasa.worldwind.cache.BasicDataFileStore Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwind.cache;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.retrieve.*;
import gov.nasa.worldwind.util.*;
import java.beans.PropertyChangeEvent;
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.util.*;
/**
* Basic implementation of {@link FileStore}.
*
* @author Tom Gaskins
* @version $Id: BasicDataFileStore.java 1950 2014-04-20 18:52:47Z tgaskins $
*/
public class BasicDataFileStore extends AbstractFileStore
{
/** The number of milliseconds to wait before a retrieval request for the same file can be reissued. */
protected static final long TIMEOUT = (long) 5e3;
/** The default content types used to determine an unknown file format in requestFile
. */
protected static final List DEFAULT_CACHE_CONTENT_TYPES = Arrays.asList(
"application/vnd.google-earth.kml+xml",
"application/vnd.google-earth.kmz",
"model/collada+xml",
"image/dds",
"image/gif",
"image/jpeg",
"image/jpg",
"image/png"
);
/** The map of cached entries. */
protected BasicMemoryCache db = new BasicMemoryCache((long) 3e5, (long) 5e5);
/**
* Absent-resource list to keep track of resources that were requested by requestFile but failed. The default list
* holds a maximum of 2000 entries, allows 3 attempts separated by 500 milliseconds before marking a resource
* semi-permanently absent, and allows additional attempts after 60 seconds. The {@link #getAbsentResourceList()}
* method may be overridden by subclasses if they wish to provide an alternatively configured absent-resource list.
*/
protected AbsentResourceList absentResources = new AbsentResourceList(2000, 3, 500, 60000);
/**
* The list of content types used to determine an unknown file format in requestFile
. If a URL is
* requested that does not have a format suffix, requestFile
appends a suffix appropriate for the
* content type returned by the server. Subsequent calls to requestFile
use the content types in this
* list to find the content type matching the cached file.
*
* This is initialized to the following list of default content types typically used in World Wind applications:
*
* - application/vnd.google-earth.kml+xml
- application/vnd.google-earth.kmz
* - model/collada+xml
- image/dds
- image/gif
- image/jpeg
- image/jpg
* - image/png
*
* This list may be overridden by specifying a comma-delimited list of content types in the World Wind configuration
* parameter gov.nasa.worldwind.avkey.CacheContentTypes
.
*/
protected List cacheContentTypes = new ArrayList(DEFAULT_CACHE_CONTENT_TYPES);
/**
* Create an instance.
*
* @throws IllegalStateException if the configuration file name cannot be determined from {@link Configuration} or
* the configuration file cannot be found.
*/
public BasicDataFileStore()
{
String configPath = Configuration.getStringValue(AVKey.DATA_FILE_STORE_CONFIGURATION_FILE_NAME);
if (configPath == null)
{
String message = Logging.getMessage("FileStore.NoConfiguration");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
java.io.InputStream is = null;
File configFile = new File(configPath);
if (configFile.exists())
{
try
{
is = new FileInputStream(configFile);
}
catch (FileNotFoundException e)
{
String message = Logging.getMessage("FileStore.LocalConfigFileNotFound", configPath);
Logging.logger().finest(message);
}
}
if (is == null)
{
is = this.getClass().getClassLoader().getResourceAsStream(configPath);
}
if (is == null)
{
String message = Logging.getMessage("FileStore.ConfigurationNotFound", configPath);
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
this.initialize(is);
}
/**
* Create an instance to manage a specified directory.
*
* @param directoryPath the directory to manage as a file store.
*/
public BasicDataFileStore(File directoryPath)
{
if (directoryPath == null)
{
String message = Logging.getMessage("nullValue.PathIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
StringBuilder sb = new StringBuilder("");
sb.append(" ");
this.initialize(WWIO.getInputStreamFromString(sb.toString()));
}
@Override
protected void initialize(InputStream xmlConfigStream)
{
super.initialize(xmlConfigStream);
String s = Configuration.getStringValue(AVKey.CACHE_CONTENT_TYPES);
if (s != null)
{
this.cacheContentTypes.clear();
String[] contentTypes = s.split(",");
for (String type : contentTypes)
{
type = type.trim();
if (!WWUtil.isEmpty(type))
this.cacheContentTypes.add(type);
}
}
}
/**
* Returns this file store's absent-resource list.
*
* @return the file store's absent-resource list.
*/
protected AbsentResourceList getAbsentResourceList()
{
return this.absentResources;
}
/**
* Returns this file store's list of content types.
*
* @return the file store's list of content types.
*/
protected List getCacheContentTypes()
{
return this.cacheContentTypes;
}
public String getContentType(String address)
{
if (address == null)
return null;
DBEntry entry = (DBEntry) this.db.getObject(address);
return entry != null ? entry.contentType : null;
}
public long getExpirationTime(String address)
{
if (address == null)
return 0;
DBEntry entry = (DBEntry) this.db.getObject(address);
return entry != null ? entry.expiration : 0;
}
/** Holds information for entries in the cache database. */
protected static class DBEntry implements Cacheable
{
protected final static int NONE = 0;
protected final static int PENDING = 1;
protected final static int LOCAL = 2;
protected String name;
protected String contentType;
protected long expiration;
protected URL localUrl;
protected long lastUpdateTime;
protected int state;
public DBEntry(String name)
{
this.name = name;
this.state = NONE;
this.lastUpdateTime = System.currentTimeMillis();
}
public long getSizeInBytes()
{
return 40 + (name != null ? 2 * name.length() : 0);
}
}
/** {@inheritDoc} */
public synchronized void removeFile(String address)
{
if (address == null)
{
String message = Logging.getMessage("nullValue.AddressIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
DBEntry entry = (DBEntry) this.db.getObject(address);
if (entry == null)
return; // Nothing to delete
// Delete the cache file
this.removeFile(entry.localUrl);
// Remove the entry from the database
this.db.remove(address);
}
/** {@inheritDoc} */
public synchronized URL requestFile(String address)
{
if (address == null)
{
String message = Logging.getMessage("nullValue.AddressIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
// Store remote files in the World Wind cache by default. This provides backward compatibility with applications
// depending on requestFile's behavior prior to the addition of the cacheRemoteFile parameter.
return this.requestFile(address, true);
}
/** {@inheritDoc} */
public synchronized URL requestFile(String address, boolean cacheRemoteFile)
{
if (address == null)
{
String message = Logging.getMessage("nullValue.AddressIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
if (this.getAbsentResourceList().isResourceAbsent(address))
return null;
DBEntry entry = (DBEntry) this.db.getObject(address);
if (entry != null)
{
long now = System.currentTimeMillis();
boolean expired = entry.expiration != 0 && now > entry.expiration;
// Return the resource if it is local and has not expired.
if (entry.state == DBEntry.LOCAL && !expired)
return entry.localUrl;
if (entry.state == DBEntry.PENDING && (now - entry.lastUpdateTime <= TIMEOUT))
return null;
}
URL url = WWIO.makeURL(address); // this may or may not make a URL, depending on address type
URL localUrl;
// If the address is already a URL in the "file" scheme, we can just use return this URL. Otherwise,
// attempt to find a local file for the address.
if (url != null && "file".equalsIgnoreCase(url.getProtocol()))
localUrl = url;
else
localUrl = this.getLocalFileUrl(address, url, cacheRemoteFile); // Don't look for temp files in the cache.
if (localUrl != null) // file exists if local URL is non-null
return localUrl;
// If the address' URL is not null but the file was not found locally, try to make it local. Store the retrieved
// file in the cache if cacheRemoteFile is true, otherwise store it in a temporary location.
if (url != null && !this.getAbsentResourceList().isResourceAbsent(address))
this.makeLocal(address, url, cacheRemoteFile);
else if (url == null)
this.getAbsentResourceList().markResourceAbsent(address); // no URL for address and not a local file
return null;
}
/**
* Returns a file from the cache, the local file system or the classpath if the file exists. The specified address
* may be a jar URL. See {@link java.net.JarURLConnection} for a description of jar URLs. If
* searchLocalCache
is true
this looks for the file in the World Wind cache, otherwise
* this only looks for the file in the local file system and the classpath.
*
* @param address the name used to identify the cached file.
* @param retrievalUrl the URL to obtain the file if it is not in the cache. Used only to determine a location
* to search in the local cache. May be null.
* @param searchLocalCache true
to look for the file in the World Wind cache, otherwise
* false
.
*
* @return the requested file if it exists, otherwise null.
*
* @throws IllegalArgumentException if the specified address is null.
*/
protected synchronized URL getLocalFileUrl(String address, URL retrievalUrl, boolean searchLocalCache)
{
if (address == null)
{
String message = Logging.getMessage("nullValue.FilePathIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
URL cacheFileUrl = null;
if (address.trim().startsWith("jar:"))
{
URL jarUrl = WWIO.makeURL(address); // retrieval URL may be other than the address' URL
if (WWIO.isLocalJarAddress(jarUrl))
{
if (this.getJarLength(jarUrl) > 0)
cacheFileUrl = jarUrl;
else
{
getAbsentResourceList().markResourceAbsent(address);
return null;
}
}
}
String addressProtocol = retrievalUrl != null ? retrievalUrl.getProtocol() : null;
if (cacheFileUrl == null && (addressProtocol == null || addressProtocol.equals("file")))
{
File f = new File(address);
if (f.exists())
try
{
cacheFileUrl = f.toURI().toURL(); // makes a file URL
}
catch (MalformedURLException e)
{
// The toURL call shouldn't fail, but continue on if it does.
}
}
// If the address is a file, look for the file in the classpath and World Wind disk cache. We perform this step
// regardless of the searchLocalCache parameter, because this looks for the file in the classpath.
// We need to ensure that the address is not a network address (HTTP, etc.) because the getResource call in
// findFile will attempt to retrieve from that URL on the thread that called this method, which might be the EDT
// (See WWJ-434).
if (cacheFileUrl == null && (addressProtocol == null || addressProtocol.equals("file")))
cacheFileUrl = WorldWind.getDataFileStore().findFile(address, true);
// Look for the file in the World Wind disk cache by creating a cache path from the file's address. We ignore this
// step if searchLocalCache is false.
if (cacheFileUrl == null && retrievalUrl != null && searchLocalCache)
{
String cachePath = this.makeCachePath(retrievalUrl, null);
cacheFileUrl = WorldWind.getDataFileStore().findFile(cachePath, true);
// If a address is requested that does not have a format suffix, then any previous call to makeLocal for the
// same address has appended a suffix to the file's cache path that is appropriate for the content type
// returned by the server. This means the address cannot be used to locate that file without knowing the
// content type. We use this file store's configurable cache content types to guess the file's content type
// and located it in the cache. Note that if the address has a suffix but is simply not in the cache, we do
// not attempt to locate it by guessing its content type.
String suffix = WWIO.getSuffix(cachePath);
if (cacheFileUrl == null && (suffix == null || suffix.length() > 4))
{
for (String contentType : this.getCacheContentTypes())
{
String pathWithSuffix = cachePath + WWIO.makeSuffixForMimeType(contentType);
cacheFileUrl = WorldWind.getDataFileStore().findFile(pathWithSuffix, true);
if (cacheFileUrl != null)
break;
}
}
}
if (cacheFileUrl != null)
{
DBEntry entry = new DBEntry(address);
entry.localUrl = cacheFileUrl;
entry.state = DBEntry.LOCAL;
entry.contentType = WWIO.makeMimeTypeForSuffix(WWIO.getSuffix(cacheFileUrl.getPath()));
this.db.add(address, entry);
this.getAbsentResourceList().unmarkResourceAbsent(address);
return cacheFileUrl;
}
return null;
}
/**
* Returns the length of the resource referred to by a jar URL. Can be used to test whether the resource exists.
*
* Note: This method causes the URL to open a connection and retrieve content length.
*
* @param jarUrl the jar URL.
*
* @return the jar file's content length, or -1 if a connection to the URL can't be formed or queried.
*/
protected int getJarLength(URL jarUrl)
{
try
{
return jarUrl.openConnection().getContentLength();
}
catch (IOException e)
{
String message = Logging.getMessage("generic.JarOpenFailed", jarUrl.toString());
Logging.logger().log(java.util.logging.Level.WARNING, message, e);
return -1;
}
}
/**
* Retrieves a specified file and either adds it to the cache or saves it in a temporary file, depending on the
* value of saveInLocalCache
.
*
* @param address the name used to identify the cached file.
* @param url the URL to obtain the file.
* @param saveInLocalCache true
to add the file to the cache, or false
to save it in a
* temporary location.
*/
protected synchronized void makeLocal(String address, URL url, boolean saveInLocalCache)
{
if (WorldWind.getNetworkStatus().isHostUnavailable(url) || !WorldWind.getRetrievalService().isAvailable())
return;
DBEntry newEntry = new DBEntry(address);
this.db.add(address, newEntry);
newEntry.state = DBEntry.PENDING;
Retriever retriever = URLRetriever.createRetriever(url, new PostProcessor(address, url, saveInLocalCache));
if (retriever != null && !WorldWind.getRetrievalService().contains(retriever))
WorldWind.getRetrievalService().runRetriever(retriever);
}
protected class PostProcessor extends AbstractRetrievalPostProcessor
{
protected String address;
protected URL retrievalUrl;
protected URL localFileUrl = null;
protected File outputFile = null;
protected boolean saveInLocalCache;
public PostProcessor(String address, URL url, boolean saveInLocalCache)
{
this.address = address;
this.retrievalUrl = url;
this.saveInLocalCache = saveInLocalCache;
}
@Override
protected boolean overwriteExistingFile()
{
return true;
}
protected File doGetOutputFile()
{
// Create the output file once and cache the result to avoid creating unused temporary files. If this
// PostProcessor's saveInLocalCache method is false, then make makeOutputFile creates a unique temporary
// file on each call. Since this method is potentially called multiple times by
// AbstractRetrievalPostProcessor, we call makeOutputFile at most one time so that only one temporary output
// file is created.
if (this.outputFile == null)
this.outputFile = this.makeOutputFile();
return this.outputFile;
}
protected File makeOutputFile()
{
File file;
String path = makeCachePath(this.retrievalUrl, this.getRetriever().getContentType());
if (this.saveInLocalCache && path.length() <= WWIO.MAX_FILE_PATH_LENGTH)
file = WorldWind.getDataFileStore().newFile(path);
else
file = BasicDataFileStore.this.makeTempFile(this.retrievalUrl, this.getRetriever().getContentType());
if (file == null)
return null;
try
{
this.localFileUrl = file.toURI().toURL();
return file;
}
catch (MalformedURLException e)
{
String message = Logging.getMessage("generic.MalformedURL", file.toURI());
Logging.logger().finest(message);
return null;
}
}
@Override
protected boolean saveBuffer() throws IOException
{
boolean tf = super.saveBuffer();
BasicDataFileStore.this.updateEntry(this.address, this.localFileUrl,
this.getRetriever().getExpirationTime());
return tf;
}
@Override
protected ByteBuffer handleSuccessfulRetrieval()
{
ByteBuffer buffer = super.handleSuccessfulRetrieval();
firePropertyChange(
new PropertyChangeEvent(BasicDataFileStore.this, AVKey.RETRIEVAL_STATE_SUCCESSFUL, this.retrievalUrl,
this.localFileUrl));
return buffer;
}
@Override
protected void markResourceAbsent()
{
BasicDataFileStore.this.getAbsentResourceList().markResourceAbsent(this.address);
}
/** {@inheritDoc} Overridden to save text files in the cache. */
@Override
protected ByteBuffer handleTextContent() throws IOException
{
this.saveBuffer();
return this.getRetriever().getBuffer();
}
}
/**
* Updates a cache entry with information available once the file is retrieved.
*
* @param address the name used to identify the file in the cache.
* @param localFileUrl the path to the local copy of the file.
* @param expiration time (in milliseconds since the Epoch) at which this entry expires, or zero to indicate that
* there is no expiration time.
*/
protected synchronized void updateEntry(String address, URL localFileUrl, long expiration)
{
DBEntry entry = (DBEntry) this.db.getObject(address);
if (entry == null)
return;
entry.state = DBEntry.LOCAL;
entry.localUrl = localFileUrl;
entry.contentType = WWIO.makeMimeTypeForSuffix(WWIO.getSuffix(localFileUrl.getPath()));
entry.expiration = expiration;
entry.lastUpdateTime = System.currentTimeMillis();
}
/**
* Makes a path to the file in the cache from the file's URL and content type.
*
* @param url the URL to obtain the file.
* @param contentType the mime type of the file's contents.
*
* @return a path name.
*/
protected String makeCachePath(URL url, String contentType)
{
if ("jar".equals(url.getProtocol()))
return this.makeJarURLCachePath(url, contentType);
return this.makeGenericURLCachePath(url, contentType);
}
/**
* Makes a path to the file in the cache from the file's generic URL and content type. If the URL has a non-empty
* query string, then this returns a path name formatted as follows:
*
* host/hashCode/path_query.suffix
*
* Otherwise, this returns a path name formatted as follows:
*
* host/hashCode/path.suffix
*
* Where host
is the name of the host machine, hashCode
is a four digit hash code computed
* from the string "path" or "path_query" (if the URL has a query string), path
is the URL's path part,
* query
is the URL's query string, and suffix
is either the path's suffix or a suffix
* created from the specified content type. The hashCode
folder is used to limit the number of files
* that appear beneath the host folder. This is necessary to avoiding the operating system's maximum file limit
* should a large number of files be requested from the same host. If two URLs have the same hash code, then both
* URLs are stored under the same hashCode
folder in the cache and are differentiated by their
* path
and query
parts.
*
* This removes any private parameters from the query string to ensure that those parameters are not written to the
* file store as part of the cache name. For example, the "CONNECTID" query parameter typically encodes a user's
* unique connection id, and must not be shared. Writing this parameter to the cache would expose that parameter to
* anyone using the same machine. If the query string is empty after removing any private parameters, it is ignored
* and only the path part of the URL is used as the filename.
*
* @param url the URL to obtain the file.
* @param contentType the mime type of the file's contents.
*
* @return a path name.
*/
protected String makeGenericURLCachePath(URL url, String contentType)
{
String host = WWIO.replaceIllegalFileNameCharacters(url.getHost());
String path = WWIO.replaceIllegalFileNameCharacters(url.getPath());
String filename = path;
if (!WWUtil.isEmpty(url.getQuery()))
{
// Remove private query parameters from the query string, and replace any illegal filename characters with
// an underscore. This avoids exposing private parameters to other users by writing them to the cache as
// part of the cache name.
String query = this.removePrivateQueryParameters(url.getQuery());
query = WWIO.replaceIllegalFileNameCharacters(query);
// If the query string is not empty after removing private parameters and illegal filename characters, we
// use it as part of the cache name by appending it to the path part.
if (!WWUtil.isEmpty(query))
{
filename = path + "_" + query;
}
}
// Create a hash folder name using the first four numbers of the filename's hash code (ignore any negative
// sign). The filename is either the path name or the path name appended with the query string. In either case,
// the same hash folder name can be re-created from the same address. If two URLs have the same hash string,
// both URLs are stored under the same hash folder and are differentiated by their filenames.
String hashString = String.valueOf(Math.abs(filename.hashCode()));
if (hashString.length() > 4)
hashString = hashString.substring(0, 4);
StringBuilder sb = new StringBuilder();
sb.append(host);
sb.append(File.separator);
sb.append(hashString);
sb.append(File.separator);
sb.append(filename);
String suffix = this.makeSuffix(filename, contentType);
if (suffix != null)
sb.append(suffix);
return sb.toString();
}
/**
* Makes a path to the file in the cache from the file's JAR URL and content type. This returns a path name
* formatted as follows:
*
* host/path.suffix
*
* Where host
is the path to the JAR file, path
is the file's path within the JAR archive,
* and suffix
is either the path's suffix or a suffix created from the specified content type.
*
* @param jarURL the URL to obtain the file. This URL is assumed to have the "jar" protocol.
* @param contentType the mime type of the file's contents.
*
* @return a path name.
*/
protected String makeJarURLCachePath(URL jarURL, String contentType)
{
String innerAddress = jarURL.getPath();
URL innerUrl = WWIO.makeURL(innerAddress);
String host = WWIO.replaceIllegalFileNameCharacters(innerUrl.getHost());
String path = WWIO.replaceIllegalFileNameCharacters(innerUrl.getPath().replace("!/", "#"));
StringBuilder sb = new StringBuilder();
sb.append(host);
sb.append(File.separator);
sb.append(path);
String suffix = this.makeSuffix(path, contentType);
if (suffix != null)
sb.append(suffix);
return sb.toString();
}
/**
* Creates a temp file to hold the contents associated with a specified URL. Since the file store intentionally does
* not persist a mapping of retrieved URLs to temp files, this deletes the returned temp file when the current Java
* Virtual Machine terminates.
*
* @param url the URL to be associated with the temp file. Used only to determine an appropriate suffix.
* @param contentType the mime type of the file contents. Used to determine the file's suffix.
*
* @return a temporary file, or null if a file could not be created.
*/
protected File makeTempFile(URL url, String contentType)
{
// Use a suffix based on the content type if the content type and the URL's suffix do not match. Otherwise
// attempt to use the URL's suffix. If neither of these attempts produce a non-null suffix, File.createTmpFile
// uses the default suffix ".tmp".
String suffix = this.makeSuffix(url.toString(), contentType); // null if the URL suffix and content type match.
if (suffix == null)
suffix = WWIO.getSuffix(url.toString());
// Ensure that the suffix starts with the "." character.
if (!suffix.startsWith("."))
suffix = "." + suffix;
try
{
File file = File.createTempFile("wwfs", suffix); // Uses the default suffix ".tmp" if this suffix is null.
file.deleteOnExit();
return file;
}
catch (IOException e)
{
String message = Logging.getMessage("generic.CannotCreateTempFile");
Logging.logger().fine(message);
return null;
}
}
/**
* Determines an appropriate suffix for a cached file. If the specified path already has a suffix that matches the
* specified content type, then this method returns null. Otherwise the method determines and returns a suffix for
* the specified content type.
*
* @param path the path whose suffix is to be validated if it exists.
* @param contentType the mime type of the data associated with the path.
*
* @return a suffix appropriate to the content type, or null if the specified path already has an appropriate
* suffix.
*/
protected String makeSuffix(String path, String contentType)
{
// If the cache path does not end in a suffix that matches the specified content type, we append the appropriate
// suffix. If the content type is not known, we do not append any suffix. If the caller does not know the
// content type used to create a cache file path, it must attempt to use known mime types until it finds a
// match.
String suffix = contentType != null ? WWIO.makeSuffixForMimeType(contentType) : null;
String existingSuffix = WWIO.getSuffix(path);
// The suffix returned by makeSuffixForMimeType is always ".jpg" for a JPEG mime type. We must convert any
// existing using "jpeg" to "jpg" to correctly match against the suffix created from the content type.
if (existingSuffix != null && existingSuffix.equalsIgnoreCase("jpeg"))
existingSuffix = "jpg";
if (suffix != null && (existingSuffix == null || !existingSuffix.equalsIgnoreCase(suffix.substring(1))))
return suffix;
else
return null;
}
/**
* Removes any private parameters from the query string to ensure that those parameters are not written to the file
* store as part of the cache name. For example, the "CONNECTID" query parameter typically encodes a user's unique
* connection id, and must not be shared. Writing this parameter to the cache would expose that parameter to anyone
* using the same machine.
*
* This removes the key, the value, and any trailing parameter delimiter of all private parameters in the specified
* query string. Recognized private query parameters are as follows:
*
* - CONNECTID
*
* @param queryString the query string to examine.
*
* @return a new string with the private query parameters removed. This string is empty if the query string is
* empty, or if the query string contains only private parameters.
*/
protected String removePrivateQueryParameters(String queryString)
{
if (WWUtil.isEmpty(queryString))
return queryString;
// Remove the "connectid" query parameter, its corresponding value, and any trailing parameter delimiter. We
// specify the regular expression directive "(?i)" to enable case-insensitive matching. The regular expression
// parameters "\Q" and "\E" define the begin and end of a literal quote around the query parameter name.
String s = queryString.replaceAll("(?i)\\Qconnectid\\E\\=[^&]*\\&?", "");
// If we removed the query string's last parameter, we need to clean up the trailing delimiter from the previous
// query parameter.
if (s.endsWith("&"))
s = s.substring(0, s.length() - 1);
return s;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy