gov.noaa.pfel.erddap.util.EDStatic.java Source code

Java tutorial

Introduction

Here is the source code for gov.noaa.pfel.erddap.util.EDStatic.java

Source

/* 
 * EDStatic Copyright 2008, NOAA.
 * See the LICENSE.txt file in this file's directory.
 */
package gov.noaa.pfel.erddap.util;

import com.cohort.array.PrimitiveArray;
import com.cohort.array.Attributes;
import com.cohort.array.StringArray;
import com.cohort.util.Calendar2;
import com.cohort.util.File2;
import com.cohort.util.Math2;
import com.cohort.util.MustBe;
import com.cohort.util.ResourceBundle2;
import com.cohort.util.SimpleException;
import com.cohort.util.String2;
import com.cohort.util.String2LogOutputStream;
import com.cohort.util.Test;
import com.cohort.util.XML;

import gov.noaa.pfel.coastwatch.griddata.NcHelper;
import gov.noaa.pfel.coastwatch.griddata.OpendapHelper;
import gov.noaa.pfel.coastwatch.pointdata.Table;
import gov.noaa.pfel.coastwatch.Projects;
import gov.noaa.pfel.coastwatch.sgt.Boundaries;
import gov.noaa.pfel.coastwatch.sgt.FilledMarkerRenderer;
import gov.noaa.pfel.coastwatch.sgt.GSHHS;
import gov.noaa.pfel.coastwatch.sgt.PathCartesianRenderer;
import gov.noaa.pfel.coastwatch.sgt.SgtGraph;
import gov.noaa.pfel.coastwatch.sgt.SgtMap;
import gov.noaa.pfel.coastwatch.sgt.SgtUtil;
import gov.noaa.pfel.coastwatch.util.HtmlWidgets;
import gov.noaa.pfel.coastwatch.util.RegexFilenameFilter;
import gov.noaa.pfel.coastwatch.util.SSR;
import gov.noaa.pfel.coastwatch.util.Tally;

import gov.noaa.pfel.erddap.*;
import gov.noaa.pfel.erddap.dataset.*;
import gov.noaa.pfel.erddap.variable.*;

import java.awt.Color;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintStream;
import java.io.Writer;
import java.security.Principal;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.FieldCache;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.Version;

//import org.apache.lucene.document.Document;
//import org.apache.lucene.document.Field;
//import org.apache.lucene.index.IndexWriter;
//import org.apache.lucene.search.TopDocs;
//import org.apache.lucene.store.SimpleFSDirectory;

//import org.verisign.joid.consumer.OpenIdFilter;

import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.nc2.Dimension;
import ucar.nc2.Group;
import ucar.nc2.NetcdfFileWriter;
import ucar.nc2.Variable;

/** 
 * This class holds a lot of static information set from the setup.xml and messages.xml
 * files and used by all the other ERDDAP classes. 
 */
public class EDStatic {

    /** The all lowercase name for the program that appears in urls. */
    public final static String programname = "erddap";

    /** The uppercase name for the program that appears on web pages. */
    public final static String ProgramName = "ERDDAP";

    /** This changes with each release. 
     * <br>See Changes information in /downloads/setup.html .
     * <br>0.1 started on 2007-09-17
     * <br>0.11 released on 2007-11-09
     * <br>0.12 released on 2007-12-05
     * <br>0.2 released on 2008-01-10
     * <br>From here on, odd .01 are used during development
     * <br>0.22 released on 2008-02-21
     * <br>0.24 released on 2008-03-03
     * <br>0.26 released on 2008-03-11
     * <br>0.28 released on 2008-04-14
     * <br>1.00 released on 2008-05-06
     * <br>1.02 released on 2008-05-26
     * <br>1.04 released on 2008-06-10
     * <br>1.06 released on 2008-06-20
     * <br>1.08 released on 2008-07-13
     * <br>1.10 released on 2008-10-14
     * <br>1.12 released on 2008-11-02
     * <br>1.14 released on 2009-03-17
     * <br>1.16 released on 2009-03-26
     * <br>1.18 released on 2009-04-08
     * <br>1.20 released on 2009-07-02
     * <br>1.22 released on 2009-07-05
     * <br>1.24 released on 2010-08-06
     * <br>1.26 released on 2010-08-25
     * <br>1.28 released on 2010-08-27
     * <br>1.30 released on 2011-04-29
     * <br>1.32 released on 2011-05-20
     * <br>1.34 released on 2011-06-15
     * <br>1.36 released on 2011-08-01
     * <br>1.38 released on 2012-04-21
     * <br>1.40 released on 2012-10-25
     * <br>1.42 released on 2012-11-26
     * <br>1.44 released on 2013-05-30
     * <br>1.46 released on 2013-07-09
     * <br>It's okay if .001 used for minor releases.
     *    Some code deals with it as a double, but never d.dd.
     * <br>1.48 released on 2014-09-04
     * <br>1.50 released on 2014-09-06
     * <br>1.52 released on 2014-10-03
     * <br>1.54 released on 2014-10-24
     * <br>1.56 released on 2014-12-16
     * <br>1.58 released on 2015-02-25
     * <br>1.60 released on 2015-03-12
     * <br>1.62 released on 2015-06-08
     * <br>1.64 released on 2015-08-19
     * <br>1.66 released on 2016-01-19
     * <br>1.68 released on 2016-02-08
     * <br>1.70 released on 2016-04-15
     * <br>1.72 released on 2016-05-12
     * <br>1.74 released on 2016-10-07
     * <br>1.76 released on 2017-05-12
     * <br>1.78 released on 2017-05-27
     * <br>1.80 released on 2017-08-04
     * <br>1.82 released on 2018-01-26
     *
     * For master branch releases, this will be a floating point
     * number with 2 decimal digits, with no additional text. 
     * !!! People other than the main ERDDAP developer (Bob) 
     * should not change the *number* below.
     * If you need to identify a fork of ERDDAP, please append "_" + other 
     * ASCII text (no spaces or control characters) to the number below,
     * e.g., "1.82_MyFork".
     * In a few places in ERDDAP, this string is parsed as a number. 
     * The parser now disregards "_" and anything following it.
     * A request to http.../erddap/version will return just the number (as text).
     * A request to http.../erddap/version_string will return the full string.
     */
    public static String erddapVersion = "1.82"; //see comment above

    /** 
     * This is almost always false.  
     * During development, Bob sets this to true. No one else needs to. 
     * If true, ERDDAP uses setup2.xml and datasets2.xml (and messages2.xml if it exists). 
     */
    public static boolean developmentMode = false;

    /** This identifies the dods server/version that this mimics. */
    public static String dapVersion = "DAP/2.0";
    public static String serverVersion = "dods/3.7"; //this is what thredds replies
    //drds at https://oceanwatch.pfeg.noaa.gov/opendap/GLOBEC/GLOBEC_bottle.ver replies "DODS/3.2"
    //both reply with server version, neither replies with coreVersion
    //spec says #.#.#, but Gallagher says #.# is fine.

    /** 
     * contentDirectory is the local directory on this computer, e.g., [tomcat]/content/erddap/ 
     * It will have a slash at the end.
     */
    public static String contentDirectory;

    public final static String INSTITUTION = "institution";
    public final static int TITLE_DOT_LENGTH = 95; //max nChar before using " ... "

    /* contextDirectory is the local directory on this computer, e.g., [tomcat]/webapps/erddap/ */
    public static String contextDirectory = SSR.getContextDirectory(); //with / separator and / at the end
    //fgdc and iso19115XmlDirectory are used for virtual URLs.
    public final static String fgdcXmlDirectory = "metadata/fgdc/xml/"; //virtual
    public final static String iso19115XmlDirectory = "metadata/iso19115/xml/"; //virtual
    public final static String DOWNLOAD_DIR = "download/";
    public final static String IMAGES_DIR = "images/";
    public final static String PUBLIC_DIR = "public/";
    public static String fullPaletteDirectory = contextDirectory + "WEB-INF/cptfiles/",
            fullPublicDirectory = contextDirectory + PUBLIC_DIR, downloadDir = contextDirectory + DOWNLOAD_DIR, //local directory on this computer
            imageDir = contextDirectory + IMAGES_DIR; //local directory on this computer
    public static Tally tally = new Tally();
    public static int failureTimesDistributionLoadDatasets[] = new int[String2.DistributionSize];
    public static int failureTimesDistribution24[] = new int[String2.DistributionSize];
    public static int failureTimesDistributionTotal[] = new int[String2.DistributionSize];
    public static int majorLoadDatasetsDistribution24[] = new int[String2.DistributionSize];
    public static int majorLoadDatasetsDistributionTotal[] = new int[String2.DistributionSize];
    public static int minorLoadDatasetsDistribution24[] = new int[String2.DistributionSize];
    public static int minorLoadDatasetsDistributionTotal[] = new int[String2.DistributionSize];
    public static int responseTimesDistributionLoadDatasets[] = new int[String2.DistributionSize];
    public static int responseTimesDistribution24[] = new int[String2.DistributionSize];
    public static int responseTimesDistributionTotal[] = new int[String2.DistributionSize];
    public static int taskThreadFailedDistribution24[] = new int[String2.DistributionSize];
    public static int taskThreadFailedDistributionTotal[] = new int[String2.DistributionSize];
    public static int taskThreadSucceededDistribution24[] = new int[String2.DistributionSize];
    public static int taskThreadSucceededDistributionTotal[] = new int[String2.DistributionSize];

    public static String datasetsThatFailedToLoad = "";
    public static String errorsDuringMajorReload = "";
    public static StringBuffer majorLoadDatasetsTimeSeriesSB = new StringBuffer(""); //thread-safe (1 thread writes but others may read)
    public static HashSet requestBlacklist = null;
    public static volatile int slowDownTroubleMillis = 1000;
    public static long startupMillis = System.currentTimeMillis();
    public static String startupLocalDateTime = Calendar2.getCurrentISODateTimeStringLocalTZ();
    public static int nGridDatasets = 0;
    public static int nTableDatasets = 0;
    public static long lastMajorLoadDatasetsStartTimeMillis = System.currentTimeMillis();
    public static long lastMajorLoadDatasetsStopTimeMillis = System.currentTimeMillis() - 1;
    private static ConcurrentHashMap<String, String> sessionNonce = new ConcurrentHashMap(16, 0.75f, 4); //for a session: loggedInAs -> nonce

    /** userHashMap. 
     * key=username (if email address, they are lowercased) 
     * value=[encoded password, sorted roles String[]] 
     * It is empty until the first LoadDatasets is finished and puts a new HashMap in place.
     * It is private so no other code can access the information except
     * via doesPasswordMatch() and getRoles().
     * MD5'd and SHA'd passwords should all already be lowercase.
     * No need to be thread-safe: one thread writes it, then put here where read only.
     */
    private static HashMap userHashMap = new HashMap();

    /** This is a HashMap of key=id value=thread that need to be interrupted/killed
     * when erddap is stopped in Tomcat.
     * For example, key="taskThread", value=taskThread.
     * The key make it easy to get a specific thread (e.g., to remove it).
     */
    public static ConcurrentHashMap runningThreads = new ConcurrentHashMap(16, 0.75f, 4);

    //taskThread variables
    //Funnelling all taskThread tasks through one taskThread ensures
    //  that the memory requirements, bandwidth usage, cpu usage,
    //  and stress on remote servers will be minimal 
    //  (although at the cost of not doing the tasks faster / in parallel).
    //In a grid of erddaps, each will have its own taskThread, which is appropriate.
    public static ArrayList taskList = new ArrayList(); //keep here in case TaskThread needs to be restarted
    private static TaskThread taskThread;
    /** lastAssignedTask is used by EDDxxxCopy instances to keep track of 
     * the number of the last task assigned to taskThread.
     * key=datasetID value=Integer(task#)
     */
    public static ConcurrentHashMap lastAssignedTask = new ConcurrentHashMap(16, 0.75f, 4);
    /** 
     * This returns the index number of the task in taskList (-1,0..) of the last completed task
     * (successful or not).
     * nFinishedTasks = lastFinishedTask + 1;
     */
    public static volatile int lastFinishedTask = -1;
    /** 
     * This returns the index number of the task in taskList (0..) that will be started
     * when the current task is finished.
     */
    public static volatile int nextTask = 0;

    /** This recieves key=startOfLocalSourceUrl value=startOfPublicSourceUrl from LoadDatasets 
     * and is used by EDD.convertToPublicSourceUrl. 
     */
    public static ConcurrentHashMap convertToPublicSourceUrl = new ConcurrentHashMap(16, 0.75f, 4);

    /** 
     * This returns the position of the "/" in if tFrom has "[something]//[something]/...",
     * and is thus a valid tFrom for convertToPublicSourceUrl.
     *
     * @return the po of the end "/" (or -1 if invalid).
     */
    public static int convertToPublicSourceUrlFromSlashPo(String tFrom) {
        if (tFrom == null)
            return -1;
        int spo = tFrom.indexOf("//");
        if (spo > 0)
            spo = tFrom.indexOf("/", spo + 2);
        return spo;
    }

    /** For Lucene. */
    //The latest version as of writing this.
    //Since I recreate the index when erddap restarted, I can change anything
    //  (e.g., Directory type, Version) any time
    //  (no worries about compatibility with existing index).
    //useful documentatino 
    //  http://lucene.apache.org/java/3_5_0/queryparsersyntax.html
    //  http://wiki.apache.org/lucene-java/LuceneFAQ
    //  http://wiki.apache.org/lucene-java/BasicsOfPerformance
    //  http://affy.blogspot.com/2003/04/codebit-examples-for-all-of-lucenes.html
    public static Version luceneVersion = Version.LUCENE_35;
    public final static String luceneDefaultField = "text";
    //special characters to be escaped
    //see bottom of http://lucene.apache.org/java/3_5_0/queryparsersyntax.html
    public static String luceneSpecialCharacters = "+-&|!(){}[]^\"~*?:\\";

    //made below if useLuceneSearchEngine
    //there are many analyzers; this is a good starting point
    public static Analyzer luceneAnalyzer;
    private static QueryParser luceneQueryParser; //not thread-safe

    //made once by RunLoadDatasets
    public static Directory luceneDirectory;
    public static IndexWriter luceneIndexWriter; //is thread-safe

    //made/returned by luceneIndexSearcher
    private static IndexReader luceneIndexReader; //is thread-safe, but only need/want one
    private static Object luceneIndexReaderLock = Calendar2.newGCalendarLocal();
    public static boolean needNewLuceneIndexReader = true;
    private static IndexSearcher luceneIndexSearcher; //is thread-safe, so can reuse
    private static String[] luceneDatasetIDFieldCache;

    //also see updateLucene in LoadDatasets

    public final static int defaultItemsPerPage = 1000; //1000, for /info/index.xxx and search
    public final static String defaultPIppQuery = "page=1&itemsPerPage=" + defaultItemsPerPage;
    public final static String allPIppQuery = "page=1&itemsPerPage=1000000000";
    /** The HTML/XML encoded form */
    public final static String encodedDefaultPIppQuery = "page=1&#x26;itemsPerPage=" + defaultItemsPerPage;
    public final static String encodedAllPIppQuery = "page=1&#x26;itemsPerPage=1000000000";
    public final static String DONT_LOG_THIS_EMAIL = "!!! DON'T LOG THIS EMAIL: ";

    /** 
     * These values are loaded from the [contentDirectory]setup.xml file. 
     * See comments in the [contentDirectory]setup.xml file.
     */
    public static String baseUrl, baseHttpsUrl, //won't be null, may be "(not specified)"
            bigParentDirectory, unitTestDataDir, unitTestBigDataDir,

            adminInstitution, adminInstitutionUrl, adminIndividualName, adminPosition, adminPhone, adminAddress,
            adminCity, adminStateOrProvince, adminPostalCode, adminCountry, adminEmail,

            accessConstraints, accessRequiresAuthorization, fees, keywords, units_standard,

            //the unencoded EDDGrid...Example attributes
            EDDGridErddapUrlExample, EDDGridIdExample, EDDGridDimensionExample, EDDGridNoHyperExample,
            EDDGridDimNamesExample, EDDGridDataTimeExample, EDDGridDataValueExample, EDDGridDataIndexExample,
            EDDGridGraphExample, EDDGridMapExample, EDDGridMatlabPlotExample,

            //variants encoded to be Html Examples
            EDDGridDimensionExampleHE, EDDGridDataIndexExampleHE, EDDGridDataValueExampleHE,
            EDDGridDataTimeExampleHE, EDDGridGraphExampleHE, EDDGridMapExampleHE,

            //variants encoded to be Html Attributes
            EDDGridDimensionExampleHA, EDDGridDataIndexExampleHA, EDDGridDataValueExampleHA,
            EDDGridDataTimeExampleHA, EDDGridGraphExampleHA, EDDGridMapExampleHA,

            //the unencoded EDDTable...Example attributes
            EDDTableErddapUrlExample, EDDTableIdExample, EDDTableVariablesExample, EDDTableConstraintsExample,
            EDDTableDataTimeExample, EDDTableDataValueExample, EDDTableGraphExample, EDDTableMapExample,
            EDDTableMatlabPlotExample,

            //variants encoded to be Html Examples
            EDDTableConstraintsExampleHE, EDDTableDataTimeExampleHE, EDDTableDataValueExampleHE,
            EDDTableGraphExampleHE, EDDTableMapExampleHE,

            //variants encoded to be Html Attributes
            EDDTableConstraintsExampleHA, EDDTableDataTimeExampleHA, EDDTableDataValueExampleHA,
            EDDTableGraphExampleHA, EDDTableMapExampleHA,

            /* For the wcs examples, pick one of your grid datasets that has longitude and latitude axes.
            The sample variable must be a variable in the sample grid dataset.
            The bounding box values are minx,miny,maxx,maxy.
            */
            wcsSampleDatasetID = "jplMURSST41", wcsSampleVariable = "analysed_sst",
            wcsSampleBBox = "-179.98,-89.98,179.98,89.98", wcsSampleAltitude = "0",
            wcsSampleTime = "2002-06-01T09:00:00Z",

            /* For the wms examples, pick one of your grid datasets that has longitude
            and latitude axes.
            The sample variable must be a variable in the sample grid dataset.
            The bounding box values are minx,miny,maxx,maxy.
            The default for wmsActive is "true".
            */
            wmsSampleDatasetID = "jplMURSST41", wmsSampleVariable = "analysed_sst",
            /* The bounding box values are minLongitude,minLatitude,maxLongitude,maxLatitude.
               Longitude values within -180 to 180, or 0 to 360, are now okay. */
            wmsSampleBBox110 = "-179.99,-89.99,180.0,89.99", wmsSampleBBox130 = "-89.99,-179.99,89.99,180.0",
            wmsSampleTime = "2002-06-01T09:00:00Z",

            sosFeatureOfInterest, sosUrnBase, sosBaseGmlName, sosStandardNamePrefix,

            authentication, //will be one of "", "custom" ["openid"]. If baseHttpsUrl doesn't start with https:, this will be "".
            datasetsRegex, drawLandMask, emailEverythingToCsv, emailDailyReportToCsv, emailSubscriptionsFrom,
            flagKeyKey, fontFamily, googleClientID, //if authentication=google, this will be something
            googleEarthLogoFile, highResLogoImageFile, legendTitle1, legendTitle2, lowResLogoImageFile,
            passwordEncoding, //will be one of "MD5", "UEPMD5", "SHA256", "UEPSHA256"
            questionMarkImageFile, searchEngine, warName;
    public static String ampLoginInfo = "&loginInfo;";
    public static String accessibleViaNC4; //"" if accessible, else message why not
    public static int lowResLogoImageFileWidth, lowResLogoImageFileHeight, highResLogoImageFileWidth,
            highResLogoImageFileHeight, googleEarthLogoFileWidth, googleEarthLogoFileHeight;
    public static Color graphBackgroundColor;
    private static String legal;
    private static int ampLoginInfoPo = -1;
    /** These are special because other loggedInAs must be String2.justPrintable
    loggedInAsHttps is for using https without being logged in, 
      but &amp;loginInfo; indicates user isn't logged in.
      It is a reserved username -- LoadDatasets prohibits defining a user with that name.
    Tab is useful here: LoadDatasets prohibits it as valid userName, 
      but it won't cause big trouble when printed in tally info.
     */
    public final static String loggedInAsHttps = "[https]"; //final so not changeable
    public final static String loggedInAsSuperuser = "\tsuperuser"; //final so not changeable
    public final static int minimumPasswordLength = 8;
    private static String startBodyHtml, endBodyHtml, startHeadHtml; //see xxx() methods

    public static boolean listPrivateDatasets, reallyVerbose, subscriptionSystemActive, convertersActive,
            slideSorterActive, fgdcActive, iso19115Active, jsonldActive, geoServicesRestActive, filesActive,
            dataProviderFormActive, outOfDateDatasetsActive, politicalBoundariesActive, wmsClientActive, sosActive,
            wcsActive, wmsActive, quickRestart, subscribeToRemoteErddapDataset, useOriginalSearchEngine,
            useLuceneSearchEngine, //exactly one will be true
            variablesMustHaveIoosCategory, verbose;
    public static String categoryAttributes[]; //as it appears in metadata (and used for hashmap)
    public static String categoryAttributesInURLs[]; //fileNameSafe (as used in URLs)
    public static boolean categoryIsGlobal[];
    public static int variableNameCategoryAttributeIndex = -1;
    public static int logMaxSizeMB, unusualActivity = 10000, partialRequestMaxBytes = 490000000, //this is just below tds default <opendap><binLimit> of 500MB
            partialRequestMaxCells = 100000;
    public static long cacheMillis, loadDatasetsMinMillis, loadDatasetsMaxMillis;
    private static String emailSmtpHost, emailUserName, emailFromAddress, emailPassword, emailProperties;
    private static int emailSmtpPort;
    private static String emailLogDate = "";
    private static BufferedWriter emailLogFile;

    //these are set as a consequence of setup.xml info
    public static SgtGraph sgtGraph;
    public static String erddapUrl, //without slash at end
            erddapHttpsUrl, //without slash at end   (may be useless, but won't be null)
            preferredErddapUrl, //without slash at end   (https if avail, else http)
            fullDatasetDirectory, //all the Directory's have slash at end
            fullCacheDirectory, fullLogsDirectory, fullCopyDirectory, fullLuceneDirectory, fullResetFlagDirectory,
            fullHardFlagDirectory, fullCptCacheDirectory, fullPlainFileNcCacheDirectory,
            fullSgtMapTopographyCacheDirectory, fullTestCacheDirectory, fullWmsCacheDirectory,

            imageDirUrl, imageDirHttpsUrl,
            //downloadDirUrl,
            computerName; //e.g., coastwatch (or "")
    public static Subscriptions subscriptions;

    /** These values are loaded from the [contentDirectory]messages.xml file (if present)
    or .../classes/gov/noaapfel/erddap/util/messages.xml. */
    public static String addConstraints, admKeywords, admSubsetVariables, admSummary, admTitle, advl_datasetID,
            advc_accessible, advl_accessible, advl_institution, advc_dataStructure, advl_dataStructure,
            advr_dataStructure, advl_cdm_data_type, advr_cdm_data_type, advl_class, advr_class, advl_title,
            advl_minLongitude, advl_maxLongitude, advl_longitudeSpacing, advl_minLatitude, advl_maxLatitude,
            advl_latitudeSpacing, advl_minAltitude, advl_maxAltitude, advl_minTime, advc_maxTime, advl_maxTime,
            advl_timeSpacing, advc_griddap, advl_griddap, advl_subset, advc_tabledap, advl_tabledap,
            advl_MakeAGraph, advc_sos, advl_sos, advl_wcs, advl_wms, advc_files, advl_files, advc_fgdc, advl_fgdc,
            advc_iso19115, advl_iso19115, advc_metadata, advl_metadata, advl_sourceUrl, advl_infoUrl, advl_rss,
            advc_email, advl_email, advl_summary, advc_testOutOfDate, advl_testOutOfDate, advc_outOfDate,
            advl_outOfDate, advn_outOfDate,

            advancedSearch, advancedSearchResults, advancedSearchDirections, advancedSearchTooltip,
            advancedSearchBounds, advancedSearchMinLat, advancedSearchMaxLat, advancedSearchMinLon,
            advancedSearchMaxLon, advancedSearchMinMaxLon, advancedSearchMinTime, advancedSearchMaxTime,
            advancedSearchClear, advancedSearchClearHelp, advancedSearchCategoryTooltip, advancedSearchRangeTooltip,
            advancedSearchMapTooltip, advancedSearchLonTooltip, advancedSearchTimeTooltip,
            advancedSearchWithCriteria, advancedSearchFewerCriteria, advancedSearchNoCriteria, autoRefresh,
            blacklistMsg, categoryTitleHtml, category1Html, category2Html, category3Html, categoryPickAttribute,
            categorySearchHtml, categorySearchDifferentHtml, categoryClickHtml, categoryNotAnOption,
            caughtInterrupted, clickAccess, clickBackgroundInfo, clickERDDAP, clickInfo, clickToSubmit,
            convertOceanicAtmosphericAcronyms, convertOceanicAtmosphericAcronymsIntro,
            convertOceanicAtmosphericAcronymsNotes, convertOceanicAtmosphericAcronymsService,
            convertOceanicAtmosphericVariableNames, convertOceanicAtmosphericVariableNamesIntro,
            convertOceanicAtmosphericVariableNamesNotes, convertOceanicAtmosphericVariableNamesService,
            convertFipsCounty, convertFipsCountyIntro, convertFipsCountyNotes, convertFipsCountyService,
            convertHtml, convertKeywords, convertKeywordsCfTooltip, convertKeywordsGcmdTooltip,
            convertKeywordsIntro, convertKeywordsNotes, convertKeywordsService, convertTime, convertTimeBypass,
            convertTimeReference, convertTimeIntro, convertTimeNotes, convertTimeService, convertTimeNumberTooltip,
            convertTimeStringTimeTooltip, convertTimeUnitsTooltip, convertTimeUnitsHelp, convertTimeIsoFormatError,
            convertTimeNoSinceError, convertTimeNumberError, convertTimeNumericTimeError,
            convertTimeParametersError, convertTimeStringFormatError, convertTimeTwoTimeError,
            convertTimeUnitsError, convertUnits, convertUnitsComparison, convertUnitsFilter, convertUnitsIntro,
            convertUnitsNotes, convertUnitsService, cookiesHelp, daf, dafGridBypassTooltip, dafGridTooltip,
            dafTableBypassTooltip, dafTableTooltip, dasTitle, dataAccessNotAllowed, databaseUnableToConnect,
            disabled, distinctValuesTooltip, doWithGraphs,

            dtAccessible, dtAccessibleYes, dtAccessibleGraphs, dtAccessibleNo, dtAccessibleLogIn, dtLogIn, dtDAF1,
            dtDAF2, dtFiles, dtMAG, dtSOS, dtSubset, dtWCS, dtWMS,

            EDDDatasetID, EDDFgdc, EDDFgdcMetadata, EDDFiles, EDDIso19115, EDDIso19115Metadata, EDDMetadata,
            EDDBackground, EDDClickOnSubmitHtml, EDDInstitution, EDDInformation, EDDSummary, EDDDatasetTitle,
            EDDDownloadData, EDDMakeAGraph, EDDMakeAMap, EDDFileType, EDDFileTypeInformation, EDDSelectFileType,
            EDDMinimum, EDDMaximum, EDDConstraint,

            EDDChangedWasnt, EDDChangedDifferentNVar, EDDChanged2Different, EDDChanged1Different,
            EDDChangedCGADifferent, EDDChangedAxesDifferentNVar, EDDChangedAxes2Different, EDDChangedAxes1Different,
            EDDChangedNoValue, EDDChangedTableToGrid,

            EDDSimilarDifferentNVar, EDDSimilarDifferent,

            EDDGridDapDescription, EDDGridDapLongDescription, EDDGridDownloadDataTooltip, EDDGridDimension,
            EDDGridDimensionRanges, EDDGridFirst, EDDGridLast, EDDGridStart, EDDGridStop, EDDGridStartStopTooltip,
            EDDGridStride, EDDGridNValues, EDDGridNValuesHtml, EDDGridSpacing, EDDGridJustOneValue, EDDGridEven,
            EDDGridUneven, EDDGridDimensionTooltip, EDDGridDimensionFirstTooltip, EDDGridDimensionLastTooltip,
            EDDGridVarHasDimTooltip, EDDGridSSSTooltip, EDDGridStartTooltip, EDDGridStopTooltip,
            EDDGridStrideTooltip, EDDGridSpacingTooltip, EDDGridDownloadTooltip, EDDGridGridVariableHtml,

            EDDTableConstraints, EDDTableTabularDatasetTooltip, EDDTableVariable, EDDTableCheckAll,
            EDDTableCheckAllTooltip, EDDTableUncheckAll, EDDTableUncheckAllTooltip, EDDTableMinimumTooltip,
            EDDTableMaximumTooltip, EDDTableCheckTheVariables, EDDTableSelectAnOperator, EDDTableOptConstraint1Html,
            EDDTableOptConstraint2Html, EDDTableOptConstraintVar, EDDTableNumericConstraintTooltip,
            EDDTableStringConstraintTooltip, EDDTableTimeConstraintTooltip, EDDTableConstraintTooltip,
            EDDTableSelectConstraintTooltip, EDDTableDapDescription, EDDTableDapLongDescription,
            EDDTableDownloadDataTooltip,

            errorTitle, errorRequestUrl, errorRequestQuery, errorTheError, errorCopyFrom, errorFileNotFound,
            errorFileNotFoundImage, errorInternal, errorJsonpFunctionName, errorJsonpNotAllowed, errorMoreThan2GB,
            errorNotFound, errorNotFoundIn, errorOdvLLTGrid, errorOdvLLTTable, errorOnWebPage, errorXWasntSpecified,
            errorXWasTooLong, externalLink, externalWebSite, fileHelp_asc, fileHelp_csv, fileHelp_csvp,
            fileHelp_csv0, fileHelp_das, fileHelp_dds, fileHelp_dods, fileHelpGrid_esriAscii, fileHelpTable_esriCsv,
            fileHelp_fgdc, fileHelp_geoJson, fileHelp_graph, fileHelpGrid_help, fileHelpTable_help, fileHelp_html,
            fileHelp_htmlTable, fileHelp_iso19115, fileHelp_itxGrid, fileHelp_itxTable, fileHelp_json,
            fileHelp_jsonlCSV, fileHelp_jsonlKVP, fileHelp_mat, fileHelpGrid_nc3, fileHelpGrid_nc4,
            fileHelpTable_nc3, fileHelpTable_nc4, fileHelp_nc3Header, fileHelp_nc4Header, fileHelp_nccsv,
            fileHelp_nccsvMetadata, fileHelp_ncCF, fileHelp_ncCFHeader, fileHelp_ncCFMA, fileHelp_ncCFMAHeader,
            fileHelp_ncml, fileHelp_ncoJson, fileHelpGrid_odvTxt, fileHelpTable_odvTxt, fileHelp_subset,
            fileHelp_timeGaps, fileHelp_tsv, fileHelp_tsvp, fileHelp_tsv0, fileHelp_wav, fileHelp_xhtml,
            fileHelp_geotif, //graphical
            fileHelpGrid_kml, fileHelpTable_kml, fileHelp_smallPdf, fileHelp_pdf, fileHelp_largePdf,
            fileHelp_smallPng, fileHelp_png, fileHelp_largePng, fileHelp_transparentPng, filesDescription,
            filesDocumentation, filesSort, filesWarning, functions, functionTooltip, functionDistinctCheck,
            functionDistinctTooltip, functionOrderByExtra, functionOrderByTooltip, functionOrderBySort,
            functionOrderBySort1, functionOrderBySort2, functionOrderBySort3, functionOrderBySort4,
            functionOrderBySortLeast, functionOrderBySortRowMax, generatedAt, geoServicesDescription,
            getStartedHtml, htmlTableMaxMessage, imageDataCourtesyOf, indexViewAll, indexSearchWith,
            indexDevelopersSearch, indexProtocol, indexDescription, indexDatasets, indexDocumentation,
            indexRESTfulSearch, indexAllDatasetsSearch, indexOpenSearch, indexServices, indexDescribeServices,
            indexMetadata, indexWAF1, indexWAF2, indexConverters, indexDescribeConverters, infoAboutFrom,
            infoTableTitleHtml, infoRequestForm, inotifyFix, justGenerateAndView, justGenerateAndViewTooltip,
            justGenerateAndViewUrl, justGenerateAndViewGraphUrlTooltip, license, listAll, listOfDatasets, LogIn,
            login, loginAttemptBlocked, loginDescribeCustom, loginDescribeEmail, loginDescribeGoogle,
            loginDescribeOpenID, loginCanNot, loginAreNot, loginToLogIn, loginEmailAddress, loginYourEmailAddress,
            loginUserName, loginPassword, loginUserNameAndPassword, loginGoogleSignIn, loginGoogleErddap,
            loginOpenID, loginOpenIDOr, loginOpenIDCreate, loginOpenIDFree, loginOpenIDSame, loginAs, loginFailed,
            loginSucceeded, loginInvalid, loginNot, loginBack, loginProblemExact, loginProblemExpire,
            loginProblemGoogleAgain, loginProblemSameBrowser, loginProblem3Times, loginProblems, loginProblemsAfter,
            loginPublicAccess, LogOut, logout, logoutOpenID, logoutSuccess, mag, magAxisX, magAxisY, magAxisColor,
            magAxisStickX, magAxisStickY, magAxisVectorX, magAxisVectorY, magAxisHelpGraphX, magAxisHelpGraphY,
            magAxisHelpMarkerColor, magAxisHelpSurfaceColor, magAxisHelpStickX, magAxisHelpStickY, magAxisHelpMapX,
            magAxisHelpMapY, magAxisHelpVectorX, magAxisHelpVectorY, magAxisVarHelp, magAxisVarHelpGrid,
            magConstraintHelp, magDocumentation, magDownload, magDownloadTooltip, magFileType, magGraphType,
            magGraphTypeTooltipGrid, magGraphTypeTooltipTable, magGS, magGSMarkerType, magGSSize, magGSColor,
            magGSColorBar, magGSColorBarTooltip, magGSContinuity, magGSContinuityTooltip, magGSScale,
            magGSScaleTooltip, magGSMin, magGSMinTooltip, magGSMax, magGSMaxTooltip, magGSNSections,
            magGSNSectionsTooltip, magGSLandMask, magGSLandMaskTooltipGrid, magGSLandMaskTooltipTable,
            magGSVectorStandard, magGSVectorStandardTooltip, magGSYAscendingTooltip, magGSYAxisMin, magGSYAxisMax,
            magGSYRangeMinTooltip, magGSYRangeMaxTooltip, magGSYRangeTooltip, magItemFirst, magItemPrevious,
            magItemNext, magItemLast, magJust1Value, magRange, magRangeTo, magRedraw, magRedrawTooltip,
            magTimeRange, magTimeRangeFirst, magTimeRangeBack, magTimeRangeForward, magTimeRangeLast,
            magTimeRangeTooltip, magTimeRangeTooltip2, magTimesVary, magViewUrl, magZoom, magZoomCenter,
            magZoomCenterTooltip, magZoomIn, magZoomInTooltip, magZoomOut, magZoomOutTooltip, magZoomALittle,
            magZoomData, magZoomOutData, magGridTooltip, magTableTooltip, metadataDownload, moreInformation,
            nMatching1, nMatching, nMatchingAlphabetical, nMatchingMostRelevant, nMatchingPage, nMatchingCurrent,
            noDataFixedValue, noDataNoLL, noDatasetWith, noPage1, noPage2, notAllowed, notAuthorized,
            notAuthorizedForData, notAvailable, noXxx, noXxxBecause, noXxxBecause2, noXxxNotActive, noXxxNoAxis1,
            noXxxNoColorBar, noXxxNoCdmDataType, noXxxNoLL, noXxxNoLLEvenlySpaced, noXxxNoLLGt1, noXxxNoLLT,
            noXxxNoLonIn180, noXxxNoNonString, noXxxNo2NonString, noXxxNoStation, noXxxNoStationID,
            noXxxNoSubsetVariables, noXxxNoOLLSubsetVariables, noXxxNoMinMax, noXxxItsGridded, noXxxItsTabular,
            optional, options, orRefineSearchWith, orSearchWith, orComma, outOfDateHtml, palettes[], palettes0[],
            paletteSections[] = { "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15",
                    "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31",
                    "32", "33", "34", "35", "36", "37", "38", "39", "40" },
            patientData, patientYourGraph, percentEncode, pickADataset, protocolSearchHtml, protocolSearch2Html,
            protocolClick,

            queryError, queryError180, queryError1Value, queryError1Var, queryError2Var, queryErrorActualRange,
            queryErrorAdjusted, queryErrorAscending, queryErrorConstraintNaN, queryErrorEqualSpacing,
            queryErrorExpectedAt, queryErrorFileType, queryErrorInvalid, queryErrorLL, queryErrorLLGt1,
            queryErrorLLT, queryErrorNeverTrue, queryErrorNeverBothTrue, queryErrorNotAxis, queryErrorNotExpectedAt,
            queryErrorNotFoundAfter, queryErrorOccursTwice, queryErrorOrderByVariable, queryErrorUnknownVariable,

            queryErrorGrid1Axis, queryErrorGridAmp, queryErrorGridDiagnostic, queryErrorGridBetween,
            queryErrorGridLessMin, queryErrorGridGreaterMax, queryErrorGridMissing, queryErrorGridNoAxisVar,
            queryErrorGridNoDataVar, queryErrorGridNotIdentical, queryErrorGridSLessS, queryErrorLastEndP,
            queryErrorLastExpected, queryErrorLastUnexpected, queryErrorLastPMInvalid, queryErrorLastPMInteger,
            rangesFromTo, resetTheForm, resetTheFormWas, resourceNotFound, requestFormatExamplesHtml,
            resultsFormatExamplesHtml, resultsOfSearchFor, restfulInformationFormats, restfulViaService, rows,
            rssNo, searchTitle, searchDoFullTextHtml, searchFullTextHtml, searchHintsLuceneTooltip,
            searchHintsOriginalTooltip, searchHintsTooltip, searchButton, searchClickTip, searchNotAvailable,
            searchTip, searchSpelling, searchFewerWords, searchWithQuery, seeProtocolDocumentation, selectNext,
            selectPrevious, sosDescriptionHtml, sosLongDescriptionHtml, sparqlP01toP02pre, sparqlP01toP02post,
            ssUse, ssUsePlain, ssBePatient, ssInstructionsHtml, standardShortDescriptionHtml, standardLicense,
            standardContact, standardDataLicenses, standardDisclaimerOfEndorsement,
            standardDisclaimerOfExternalLinks, standardGeneralDisclaimer, standardPrivacyPolicy, statusHtml, submit,
            submitTooltip, subscriptionsTitle, subscriptionAdd, subscriptionAddHtml, subscriptionValidate,
            subscriptionValidateHtml, subscriptionList, subscriptionListHtml, subscriptionRemove,
            subscriptionRemoveHtml, subscriptionAbuse, subscriptionAddError, subscriptionAdd2,
            subscriptionAddSuccess, subscriptionEmail, subscriptionEmailInvalid, subscriptionEmailTooLong,
            subscriptionEmailUnspecified, subscription0Html, subscription1Html, subscription2Html,
            subscriptionIDInvalid, subscriptionIDTooLong, subscriptionIDUnspecified, subscriptionKeyInvalid,
            subscriptionKeyUnspecified, subscriptionListError, subscriptionListSuccess, subscriptionRemoveError,
            subscriptionRemove2, subscriptionRemoveSuccess, subscriptionRSS, subscriptionsNotAvailable,
            subscriptionUrlHtml, subscriptionUrlInvalid, subscriptionUrlTooLong, subscriptionValidateError,
            subscriptionValidateSuccess, subset, subsetSelect, subsetNMatching, subsetInstructions, subsetOption,
            subsetOptions, subsetRefineMapDownload, subsetRefineSubsetDownload, subsetClickResetClosest,
            subsetClickResetLL, subsetMetadata, subsetCount, subsetPercent, subsetViewSelect,
            subsetViewSelectDistinctCombos, subsetViewSelectRelatedCounts, subsetWhen, subsetWhenNoConstraints,
            subsetWhenCounts, subsetComboClickSelect, subsetNVariableCombos, subsetShowingAllRows,
            subsetShowingNRows, subsetChangeShowing, subsetNRowsRelatedData, subsetViewRelatedChange,
            subsetTotalCount, subsetView, subsetViewCheck, subsetViewCheck1, subsetViewDistinctMap,
            subsetViewRelatedMap, subsetViewDistinctDataCounts, subsetViewDistinctData, subsetViewRelatedDataCounts,
            subsetViewRelatedData, subsetViewDistinctMapTooltip, subsetViewRelatedMapTooltip,
            subsetViewDistinctDataCountsTooltip, subsetViewDistinctDataTooltip, subsetViewRelatedDataCountsTooltip,
            subsetViewRelatedDataTooltip, subsetWarn, subsetWarn10000, subsetTooltip, subsetNotSetUp,
            subsetLongNotShown,

            tabledapVideoIntro, Then, unknownDatasetID, unknownProtocol, unsupportedFileType, updateUrlsFrom[],
            updateUrlsTo[], updateUrlsSkipAttributes[], viewAllDatasetsHtml, waitThenTryAgain, warning,

            wcsDescriptionHtml, wcsLongDescriptionHtml,

            wmsDescriptionHtml, wmsInstructions, wmsLongDescriptionHtml, wmsManyDatasets;

    public static int[] imageWidths, imageHeights, pdfWidths, pdfHeights;
    private static String theShortDescriptionHtml, theLongDescriptionHtml; //see the xxx() methods
    public static String errorFromDataSource = String2.ERROR + " from data source: ";

    /** These are only created/used by GenerateDatasetsXml threads. 
     *  See the related methods below that create them.
     */
    private static Table gdxAcronymsTable;
    private static HashMap<String, String> gdxAcronymsHashMap, gdxVariableNamesHashMap;

    /** This static block reads this class's static String values from
     * contentDirectory, which must contain setup.xml and datasets.xml 
     * (and may contain messages.xml).
     * It may be a defined environment variable ("erddapContentDirectory")
     * or a subdir of <tomcat> (e.g., usr/local/tomcat/content/erddap/)
     *   (more specifically, a sibling of 'tomcat'/webapps).
     *
     * @throws RuntimeException if trouble
     */
    static {

        String erdStartup = "ERD Low Level Startup";
        String errorInMethod = "";
        try {

            //route calls to a logger to com.cohort.util.String2Log
            String2.setupCommonsLogging(-1);

            String eol = String2.lineSeparator;
            String2.log(eol + "////**** " + erdStartup + eol + "localTime="
                    + Calendar2.getCurrentISODateTimeStringLocalTZ() + eol + String2.standardHelpAboutMessage());

            //**** find contentDirectory
            String ecd = "erddapContentDirectory"; //the name of the environment variable
            errorInMethod = "Couldn't find 'content' directory ([tomcat]/content/erddap/ ?) " + "because '" + ecd
                    + "' environment variable not found " + "and couldn't find '/webapps/' in classPath="
                    + String2.getClassPath() + //with / separator and / at the end
                    " (and 'content/erddap' should be a sibling of <tomcat>/webapps): ";
            contentDirectory = System.getProperty(ecd);
            if (contentDirectory == null) {
                //Or, it must be sibling of webapps
                //e.g., c:/programs/_tomcat/webapps/erddap/WEB-INF/classes/[these classes]
                //On windows, contentDirectory may have spaces as %20(!)
                contentDirectory = String2.replaceAll(String2.getClassPath(), //with / separator and / at the end
                        "%20", " ");
                int po = contentDirectory.indexOf("/webapps/");
                contentDirectory = contentDirectory.substring(0, po) + "/content/erddap/"; //exception if po=-1
            } else {
                contentDirectory = File2.addSlash(contentDirectory);
            }
            Test.ensureTrue(File2.isDirectory(contentDirectory),
                    "contentDirectory (" + contentDirectory + ") doesn't exist.");

            //**** setup.xml *************************************************************
            //read static Strings from setup.xml 
            String setupFileName = contentDirectory + "setup" + (developmentMode ? "2" : "") + ".xml";
            errorInMethod = "ERROR while reading " + setupFileName + ": ";
            ResourceBundle2 setup = ResourceBundle2.fromXml(XML.parseXml(setupFileName, false));

            //logLevel may be: warning, info(default), all
            String logLevel = setup.getString("logLevel", "info").toLowerCase();
            verbose = !logLevel.equals("warning");
            AxisDataAccessor.verbose = verbose;
            Boundaries.verbose = verbose;
            Calendar2.verbose = verbose;
            EDD.verbose = verbose;
            EDV.verbose = verbose;
            Erddap.verbose = verbose;
            File2.verbose = verbose;
            FilledMarkerRenderer.verbose = verbose;
            gov.noaa.pfel.coastwatch.griddata.Grid.verbose = verbose;
            GridDataAccessor.verbose = verbose;
            GSHHS.verbose = verbose;
            LoadDatasets.verbose = verbose;
            NcHelper.verbose = verbose;
            OutputStreamFromHttpResponse.verbose = verbose;
            PathCartesianRenderer.verbose = verbose;
            Projects.verbose = verbose;
            //ResourceBundle2.verbose = verbose;
            RunLoadDatasets.verbose = verbose;
            SgtGraph.verbose = verbose;
            SgtMap.verbose = verbose;
            SgtUtil.verbose = verbose;
            SSR.verbose = verbose;
            Subscriptions.verbose = verbose;
            Table.verbose = verbose;
            TaskThread.verbose = verbose;

            reallyVerbose = logLevel.equals("all");
            AxisDataAccessor.reallyVerbose = reallyVerbose;
            Boundaries.reallyVerbose = reallyVerbose;
            Calendar2.reallyVerbose = reallyVerbose;
            EDD.reallyVerbose = reallyVerbose;
            EDV.reallyVerbose = reallyVerbose;
            File2.reallyVerbose = reallyVerbose;
            FilledMarkerRenderer.reallyVerbose = reallyVerbose;
            GridDataAccessor.reallyVerbose = reallyVerbose;
            GSHHS.reallyVerbose = reallyVerbose;
            LoadDatasets.reallyVerbose = reallyVerbose;
            NcHelper.reallyVerbose = reallyVerbose;
            PathCartesianRenderer.reallyVerbose = reallyVerbose;
            SgtGraph.reallyVerbose = reallyVerbose;
            SgtMap.reallyVerbose = reallyVerbose;
            SgtUtil.reallyVerbose = reallyVerbose;
            SSR.reallyVerbose = reallyVerbose;
            Subscriptions.reallyVerbose = reallyVerbose;
            Table.reallyVerbose = reallyVerbose;
            //Table.debug = reallyVerbose; //for debugging
            TaskThread.reallyVerbose = reallyVerbose;

            bigParentDirectory = setup.getNotNothingString("bigParentDirectory", "");
            bigParentDirectory = File2.addSlash(bigParentDirectory);
            Test.ensureTrue(File2.isDirectory(bigParentDirectory),
                    "bigParentDirectory (" + bigParentDirectory + ") doesn't exist.");
            unitTestDataDir = setup.getString("unitTestDataDir", "[specify <unitTestDataDir> in setup.xml]");
            unitTestBigDataDir = setup.getString("unitTestBigDataDir",
                    "[specify <unitTestBigDataDir> in setup.xml]");
            unitTestDataDir = File2.addSlash(unitTestDataDir);
            unitTestBigDataDir = File2.addSlash(unitTestBigDataDir);
            String2.unitTestDataDir = unitTestDataDir;
            String2.unitTestBigDataDir = unitTestBigDataDir;

            //email  (do early on so email can be sent if trouble later in this method)
            emailSmtpHost = setup.getString("emailSmtpHost", null);
            emailSmtpPort = setup.getInt("emailSmtpPort", 25);
            emailUserName = setup.getString("emailUserName", null);
            emailPassword = setup.getString("emailPassword", null);
            emailProperties = setup.getString("emailProperties", null);
            emailFromAddress = setup.getString("emailFromAddress", null);
            emailEverythingToCsv = setup.getString("emailEverythingTo", ""); //won't be null
            emailDailyReportToCsv = setup.getString("emailDailyReportTo", ""); //won't be null

            String tsar[] = String2.split(emailEverythingToCsv, ',');
            if (emailEverythingToCsv.length() > 0)
                for (int i = 0; i < tsar.length; i++)
                    if (!String2.isEmailAddress(tsar[i]) || tsar[i].startsWith("your.")) //prohibit the default email addresses
                        throw new RuntimeException(
                                "setup.xml error: invalid email address=" + tsar[i] + " in <emailEverythingTo>.");
            emailSubscriptionsFrom = tsar.length > 0 ? tsar[0] : ""; //won't be null

            tsar = String2.split(emailDailyReportToCsv, ',');
            if (emailDailyReportToCsv.length() > 0)
                for (int i = 0; i < tsar.length; i++)
                    if (!String2.isEmailAddress(tsar[i]) || tsar[i].startsWith("your.")) //prohibit the default email addresses
                        throw new RuntimeException(
                                "setup.xml error: invalid email address=" + tsar[i] + " in <emailDailyReportTo>.");

            //test of email
            //Test.error("This is a test of emailing an error in Erddap constructor.");

            //2014-09-03 deleting all cache and public files was moved from here to ERDDAP constructor

            //*** set up directories  //all with slashes at end
            //before 2011-12-30, was fullDatasetInfoDirectory datasetInfo/; see conversion below
            fullDatasetDirectory = bigParentDirectory + "dataset/";
            fullCacheDirectory = bigParentDirectory + "cache/";
            fullResetFlagDirectory = bigParentDirectory + "flag/";
            fullHardFlagDirectory = bigParentDirectory + "hardFlag/";
            fullLogsDirectory = bigParentDirectory + "logs/";
            fullCopyDirectory = bigParentDirectory + "copy/";
            fullLuceneDirectory = bigParentDirectory + "lucene/";

            Test.ensureTrue(File2.isDirectory(fullPaletteDirectory),
                    "fullPaletteDirectory (" + fullPaletteDirectory + ") doesn't exist.");
            errorInMethod = "ERROR while creating directories: "; //File2.makeDir throws exception if failure
            File2.makeDirectory(fullPublicDirectory); //make it, because Git doesn't track empty dirs
            File2.makeDirectory(fullDatasetDirectory);
            File2.makeDirectory(fullCacheDirectory);
            File2.makeDirectory(fullResetFlagDirectory);
            File2.makeDirectory(fullHardFlagDirectory);
            File2.makeDirectory(fullLogsDirectory);
            File2.makeDirectory(fullCopyDirectory);
            File2.makeDirectory(fullLuceneDirectory);

            String2.log("logLevel=" + logLevel + ": verbose=" + verbose + " reallyVerbose=" + reallyVerbose + eol
                    + "bigParentDirectory=" + bigParentDirectory + eol + "contextDirectory=" + contextDirectory);

            //are bufferedImages hardware accelerated?
            String2.log(SgtUtil.isBufferedImageAccelerated());

            //2011-12-30 convert /datasetInfo/[datasetID]/ to 
            //                   /dataset/[last2char]/[datasetID]/
            //to prepare for huge number of datasets
            String oldBaseDir = bigParentDirectory + "datasetInfo/"; //the old name
            if (File2.isDirectory(oldBaseDir)) {
                errorInMethod = "ERROR while converting from oldBaseDir=" + oldBaseDir + ": ";
                try {
                    String2.log("[[converting datasetInfo/ to dataset/");
                    String oldBaseDirList[] = (new File(oldBaseDir)).list();
                    int oldBaseDirListSize = oldBaseDirList == null ? 0 : oldBaseDirList.length;
                    for (int od = 0; od < oldBaseDirListSize; od++) {
                        String odName = oldBaseDirList[od];
                        if (File2.isFile(oldBaseDir + odName)) {
                            //delete obsolete files
                            File2.delete(oldBaseDir + odName);
                            continue;
                        }
                        if (!File2.isDirectory(oldBaseDir + odName)) {
                            //link??
                            continue;
                        }
                        String fullNdName = EDD.datasetDir(odName);
                        File2.makeDirectory(fullNdName);
                        String oldFileList[] = (new File(oldBaseDir + odName)).list();
                        int oldFileListSize = oldFileList == null ? 0 : oldFileList.length;
                        for (int of = 0; of < oldFileListSize; of++) {
                            String ofName = oldFileList[of];
                            String fullOfName = oldBaseDir + odName + "/" + ofName;
                            if (!ofName.matches(".*[0-9]{7}")) //skip temp files
                                File2.copy(fullOfName, fullNdName + ofName); //dir will be created
                            File2.delete(fullOfName);
                        }
                        File2.deleteAllFiles(oldBaseDir + odName); //should be already empty
                    }
                    File2.deleteAllFiles(oldBaseDir, true, true); //and delete empty subdir
                    File2.delete(oldBaseDir); //hopefully empty
                    String2.log("]]datasetInfo/ was successfully converted to dataset/");

                } catch (Throwable t) {
                    String2.log("WARNING: " + MustBe.throwableToString(t));
                }
            }

            //deal with cache
            //how many millis should files be left in the cache (if untouched)?
            cacheMillis = setup.getInt("cacheMinutes", 60) * 60000L; // millis/min

            //make some subdirectories of fullCacheDirectory
            //'_' distinguishes from dataset cache dirs
            errorInMethod = "ERROR while creating directories: ";
            fullCptCacheDirectory = fullCacheDirectory + "_cpt/";
            fullPlainFileNcCacheDirectory = fullCacheDirectory + "_plainFileNc/";
            fullSgtMapTopographyCacheDirectory = fullCacheDirectory + "_SgtMapTopography/";
            fullTestCacheDirectory = fullCacheDirectory + "_test/";
            fullWmsCacheDirectory = fullCacheDirectory + "_wms/"; //for all-datasets WMS and subdirs for non-data layers
            File2.makeDirectory(fullCptCacheDirectory);
            File2.makeDirectory(fullPlainFileNcCacheDirectory);
            File2.makeDirectory(fullSgtMapTopographyCacheDirectory);
            File2.makeDirectory(fullTestCacheDirectory);
            File2.makeDirectory(fullWmsCacheDirectory);
            File2.makeDirectory(fullWmsCacheDirectory + "Land"); //includes LandMask
            File2.makeDirectory(fullWmsCacheDirectory + "Coastlines");
            File2.makeDirectory(fullWmsCacheDirectory + "LakesAndRivers");
            File2.makeDirectory(fullWmsCacheDirectory + "Nations");
            File2.makeDirectory(fullWmsCacheDirectory + "States");

            //get other info from setup.xml
            errorInMethod = "ERROR while reading " + setupFileName + ": ";
            baseUrl = setup.getNotNothingString("baseUrl", errorInMethod);
            baseHttpsUrl = setup.getString("baseHttpsUrl", "(not specified)"); //not "" (to avoid relative urls)
            categoryAttributes = String2.split(setup.getNotNothingString("categoryAttributes", ""), ',');
            int nCat = categoryAttributes.length;
            categoryAttributesInURLs = new String[nCat];
            categoryIsGlobal = new boolean[nCat]; //initially all false
            for (int cati = 0; cati < nCat; cati++) {
                String cat = categoryAttributes[cati];
                if (cat.startsWith("global:")) {
                    categoryIsGlobal[cati] = true;
                    cat = cat.substring(7);
                    categoryAttributes[cati] = cat;
                } else if (cat.equals("institution")) { //legacy special case
                    categoryIsGlobal[cati] = true;
                }
                categoryAttributesInURLs[cati] = String2.modifyToBeFileNameSafe(cat);
            }
            variableNameCategoryAttributeIndex = String2.indexOf(categoryAttributes, "variableName");

            String wmsActiveString = setup.getString("wmsActive", "");
            wmsActive = String2.isSomething(wmsActiveString) ? String2.parseBoolean(wmsActiveString) : true;
            wmsSampleDatasetID = setup.getString("wmsSampleDatasetID", wmsSampleDatasetID);
            wmsSampleVariable = setup.getString("wmsSampleVariable", wmsSampleVariable);
            wmsSampleBBox110 = setup.getString("wmsSampleBBox110", wmsSampleBBox110);
            wmsSampleBBox130 = setup.getString("wmsSampleBBox130", wmsSampleBBox130);
            wmsSampleTime = setup.getString("wmsSampleTime", wmsSampleTime);

            adminInstitution = setup.getNotNothingString("adminInstitution", errorInMethod);
            adminInstitutionUrl = setup.getNotNothingString("adminInstitutionUrl", errorInMethod);
            adminIndividualName = setup.getNotNothingString("adminIndividualName", errorInMethod);
            adminPosition = setup.getNotNothingString("adminPosition", errorInMethod);
            adminPhone = setup.getNotNothingString("adminPhone", errorInMethod);
            adminAddress = setup.getNotNothingString("adminAddress", errorInMethod);
            adminCity = setup.getNotNothingString("adminCity", errorInMethod);
            adminStateOrProvince = setup.getNotNothingString("adminStateOrProvince", errorInMethod);
            adminPostalCode = setup.getNotNothingString("adminPostalCode", errorInMethod);
            adminCountry = setup.getNotNothingString("adminCountry", errorInMethod);
            adminEmail = setup.getNotNothingString("adminEmail", errorInMethod);

            if (adminInstitution.startsWith("Your"))
                throw new RuntimeException("setup.xml error: invalid <adminInstitution>=" + adminInstitution);
            if (!adminInstitutionUrl.startsWith("http") || !String2.isUrl(adminInstitutionUrl))
                throw new RuntimeException("setup.xml error: invalid <adminInstitutionUrl>=" + adminInstitutionUrl);
            if (adminIndividualName.startsWith("Your"))
                throw new RuntimeException("setup.xml error: invalid <adminIndividualName>=" + adminIndividualName);
            //if (adminPosition.length() == 0)
            //    throw new RuntimeException("setup.xml error: invalid <adminPosition>=" + adminPosition);             
            if (adminPhone.indexOf("999-999") >= 0)
                throw new RuntimeException("setup.xml error: invalid <adminPhone>=" + adminPhone);
            if (adminAddress.equals("123 Main St."))
                throw new RuntimeException("setup.xml error: invalid <adminAddress>=" + adminAddress);
            if (adminCity.equals("Some Town"))
                throw new RuntimeException("setup.xml error: invalid <adminCity>=" + adminCity);
            //if (adminStateOrProvince.length() == 0)
            //    throw new RuntimeException("setup.xml error: invalid <adminStateOrProvince>=" + adminStateOrProvince);  
            if (adminPostalCode.equals("99999"))
                throw new RuntimeException("setup.xml error: invalid <adminPostalCode>=" + adminPostalCode);
            //if (adminCountry.length() == 0)
            //    throw new RuntimeException("setup.xml error: invalid <adminCountry>=" + adminCountry);  
            if (!String2.isEmailAddress(adminEmail) || adminEmail.startsWith("your."))
                throw new RuntimeException("setup.xml error: invalid <adminEmail>=" + adminEmail);

            accessConstraints = setup.getNotNothingString("accessConstraints", errorInMethod);
            accessRequiresAuthorization = setup.getNotNothingString("accessRequiresAuthorization", errorInMethod);
            fees = setup.getNotNothingString("fees", errorInMethod);
            keywords = setup.getNotNothingString("keywords", errorInMethod);
            legal = setup.getNotNothingString("legal", errorInMethod);

            units_standard = setup.getString("units_standard", "UDUNITS");

            fgdcActive = setup.getBoolean("fgdcActive", true);
            iso19115Active = setup.getBoolean("iso19115Active", true);
            jsonldActive = setup.getBoolean("jsonldActive", true);
            //until geoServicesRest is finished, it is always inactive
            geoServicesRestActive = false; //setup.getBoolean(         "geoServicesRestActive",      false); 
            filesActive = setup.getBoolean("filesActive", true);
            dataProviderFormActive = setup.getBoolean("dataProviderFormActive", true);
            outOfDateDatasetsActive = setup.getBoolean("outOfDateDatasetsActive", true);
            politicalBoundariesActive = setup.getBoolean("politicalBoundariesActive", true);
            wmsClientActive = setup.getBoolean("wmsClientActive", true);
            SgtMap.drawPoliticalBoundaries = politicalBoundariesActive;

            //until SOS is finished, it is always inactive
            sosActive = false;//        sosActive                  = setup.getBoolean(         "sosActive",                  false); 
            if (sosActive) {
                sosFeatureOfInterest = setup.getNotNothingString("sosFeatureOfInterest", errorInMethod);
                sosStandardNamePrefix = setup.getNotNothingString("sosStandardNamePrefix", errorInMethod);
                sosUrnBase = setup.getNotNothingString("sosUrnBase", errorInMethod);

                //make the sosGmlName, e.g., http://coastwatch.pfeg.noaa.gov:8080 -> gov.noaa.pfeg.coastwatch
                sosBaseGmlName = baseUrl;
                int po = sosBaseGmlName.indexOf("//");
                if (po > 0)
                    sosBaseGmlName = sosBaseGmlName.substring(po + 2);
                po = sosBaseGmlName.indexOf(":");
                if (po > 0)
                    sosBaseGmlName = sosBaseGmlName.substring(0, po);
                StringArray sbgn = new StringArray(String2.split(sosBaseGmlName, '.'));
                sbgn.reverse();
                sosBaseGmlName = String2.toSVString(sbgn.toArray(), ".", false);
            }

            //until it is finished, it is always inactive
            wcsActive = false; //setup.getBoolean(         "wcsActive",                  false); 

            authentication = setup.getString("authentication", "");
            datasetsRegex = setup.getString("datasetsRegex", ".*");
            drawLandMask = setup.getString("drawLandMask", null);
            if (drawLandMask == null) //2014-08-28 changed defaults below to "under". It will be in v1.48
                drawLandMask = setup.getString("drawLand", "under");
            if (!drawLandMask.equals("under") && !drawLandMask.equals("over"))
                drawLandMask = "under"; //default
            flagKeyKey = setup.getNotNothingString("flagKeyKey", errorInMethod);
            if (flagKeyKey.toUpperCase().indexOf("CHANGE THIS") >= 0)
                //really old default: "A stitch in time saves nine. CHANGE THIS!!!"  
                //current default:    "CHANGE THIS TO YOUR FAVORITE QUOTE"
                throw new RuntimeException(String2.ERROR
                        + ": You must change the <flagKeyKey> in setup.xml to a new, unique, non-default value. "
                        + "NOTE that this will cause the flagKeys used by your datasets to change. "
                        + "Any subscriptions using the old flagKeys will need to be redone.");
            fontFamily = setup.getString("fontFamily", "SansSerif");
            graphBackgroundColor = new Color(
                    String2.parseInt(setup.getString("graphBackgroundColor", "0xffccccff")), true); //hasAlpha
            googleClientID = setup.getString("googleClientID", null);
            googleEarthLogoFile = setup.getNotNothingString("googleEarthLogoFile", errorInMethod);
            highResLogoImageFile = setup.getNotNothingString("highResLogoImageFile", errorInMethod);
            legendTitle1 = setup.getString("legendTitle1", null);
            legendTitle2 = setup.getString("legendTitle2", null);
            listPrivateDatasets = setup.getBoolean("listPrivateDatasets", false);
            loadDatasetsMinMillis = Math.max(1, setup.getInt("loadDatasetsMinMinutes", 15)) * 60000L;
            loadDatasetsMaxMillis = setup.getInt("loadDatasetsMaxMinutes", 60) * 60000L;
            loadDatasetsMaxMillis = Math.max(loadDatasetsMinMillis * 2, loadDatasetsMaxMillis);
            logMaxSizeMB = Math2.minMax(1, 2000, setup.getInt("logMaxSizeMB", 20)); //2048MB=2GB
            lowResLogoImageFile = setup.getNotNothingString("lowResLogoImageFile", errorInMethod);
            partialRequestMaxBytes = setup.getInt("partialRequestMaxBytes", partialRequestMaxBytes);
            partialRequestMaxCells = setup.getInt("partialRequestMaxCells", partialRequestMaxCells);
            questionMarkImageFile = setup.getNotNothingString("questionMarkImageFile", errorInMethod);
            quickRestart = setup.getBoolean("quickRestart", true);
            passwordEncoding = setup.getString("passwordEncoding", "UEPSHA256");
            searchEngine = setup.getString("searchEngine", "original");

            subscribeToRemoteErddapDataset = setup.getBoolean("subscribeToRemoteErddapDataset", true);
            subscriptionSystemActive = setup.getBoolean("subscriptionSystemActive", true);
            convertersActive = setup.getBoolean("convertersActive", true);
            slideSorterActive = setup.getBoolean("slideSorterActive", true);
            theShortDescriptionHtml = setup.getNotNothingString("theShortDescriptionHtml", errorInMethod);
            unusualActivity = setup.getInt("unusualActivity", unusualActivity);
            variablesMustHaveIoosCategory = setup.getBoolean("variablesMustHaveIoosCategory", true);
            warName = setup.getString("warName", "erddap");

            //use Lucence?
            if (searchEngine.equals("lucene")) {
                useLuceneSearchEngine = true;
            } else {
                Test.ensureEqual(searchEngine, "original",
                        "<searchEngine> must be \"original\" (the default) or \"lucene\".");
                useOriginalSearchEngine = true;
            }

            errorInMethod = "ERROR while initializing SgtGraph: ";
            sgtGraph = new SgtGraph(fontFamily);

            //ensure erddapVersion is okay
            int upo = erddapVersion.indexOf('_');
            double ev = String2.parseDouble(upo >= 0 ? erddapVersion.substring(0, upo) : erddapVersion);
            if (upo == -1 && erddapVersion.length() == 4 && ev > 1.8 && ev < 10) {
            } //it's just a number
            else if ((upo != -1 && upo != 4) || ev <= 1.8 || ev >= 10 || Double.isNaN(ev)
                    || erddapVersion.indexOf(' ') >= 0 || !String2.isAsciiPrintable(erddapVersion))
                throw new SimpleException(
                        "The format of EDStatic.erddapVersion must be d.dd[_someAsciiText]. (ev=" + ev + ")");

            //ensure authentication setup is okay
            errorInMethod = "ERROR while checking authentication setup: ";
            if (authentication == null)
                authentication = "";
            authentication = authentication.trim().toLowerCase();
            if (!authentication.equals("") && !authentication.equals("custom") && !authentication.equals("email")
                    && !authentication.equals("google")
            //&& !authentication.equals("openid")  //OUT-OF-DATE
            )
                throw new RuntimeException("setup.xml error: authentication=" + authentication
                        + " must be (nothing)|custom|google|email."); //was also "openid"  //google NOT FINISHED
            if (!authentication.equals("") && !baseHttpsUrl.startsWith("https://"))
                throw new RuntimeException(
                        "setup.xml error: " + ": For any <authentication> other than \"\", the baseHttpsUrl="
                                + baseHttpsUrl + " must start with \"https://\".");
            if (authentication.equals("google") && !String2.isSomething(googleClientID))
                throw new RuntimeException("setup.xml error: "
                        + ": When authentication=google, you must provide your <googleClientID>.");
            if (authentication.equals("custom")
                    && (!passwordEncoding.equals("MD5") && !passwordEncoding.equals("UEPMD5")
                            && !passwordEncoding.equals("SHA256") && !passwordEncoding.equals("UEPSHA256")))
                throw new RuntimeException("setup.xml error: When authentication=custom, passwordEncoding="
                        + passwordEncoding + " must be MD5|UEPMD5|SHA256|UEPSHA256.");
            //String2.log("authentication=" + authentication);

            //things set as a consequence of setup.xml
            erddapUrl = baseUrl + "/" + warName;
            erddapHttpsUrl = baseHttpsUrl + "/" + warName;
            preferredErddapUrl = baseHttpsUrl.startsWith("https://") ? erddapHttpsUrl : erddapUrl;
            imageDirUrl = erddapUrl + "/" + IMAGES_DIR;
            imageDirHttpsUrl = erddapHttpsUrl + "/" + IMAGES_DIR;
            //downloadDirUrl   = erddapUrl      + "/" + DOWNLOAD_DIR;  //if uncommented, you need downloadDirHttpsUrl too

            //???if logoImgTag is needed, convert to method logoImgTag(loggedInAs)
            //logoImgTag = "      <img src=\"" + imageDirUrl(loggedInAs) + lowResLogoImageFile + "\" " +
            //    "alt=\"logo\" title=\"logo\">\n";

            //**** messages.xml *************************************************************
            //read static messages from messages(2).xml in contentDirectory?
            String messagesFileName = contentDirectory + "messages" + (developmentMode ? "2" : "") + ".xml";
            if (File2.isFile(messagesFileName)) {
                String2.log("Using custom messages.xml from " + messagesFileName);
            } else {
                //use default messages.xml
                String2.log("Custom messages.xml not found at " + messagesFileName);
                //use String2.getClass(), not ClassLoader.getSystemResource (which fails in Tomcat)
                messagesFileName = String2.getClassPath() + //with / separator and / at the end
                        "gov/noaa/pfel/erddap/util/messages.xml";
                String2.log("Using default messages.xml from  " + messagesFileName);
            }
            errorInMethod = "ERROR while reading messages.xml: ";
            ResourceBundle2 messages = ResourceBundle2.fromXml(XML.parseXml(messagesFileName, false));

            //read all the static Strings from messages.xml
            addConstraints = messages.getNotNothingString("addConstraints", errorInMethod);
            admKeywords = messages.getNotNothingString("admKeywords", errorInMethod);
            admSubsetVariables = messages.getNotNothingString("admSubsetVariables", errorInMethod);
            admSummary = messages.getNotNothingString("admSummary", errorInMethod);
            admTitle = messages.getNotNothingString("admTitle", errorInMethod);
            advl_datasetID = messages.getNotNothingString("advl_datasetID", errorInMethod);
            advc_accessible = messages.getNotNothingString("advc_accessible", errorInMethod);
            advl_accessible = messages.getNotNothingString("advl_accessible", errorInMethod);
            advl_institution = messages.getNotNothingString("advl_institution", errorInMethod);
            advc_dataStructure = messages.getNotNothingString("advc_dataStructure", errorInMethod);
            advl_dataStructure = messages.getNotNothingString("advl_dataStructure", errorInMethod);
            advr_dataStructure = messages.getNotNothingString("advr_dataStructure", errorInMethod);
            advl_cdm_data_type = messages.getNotNothingString("advl_cdm_data_type", errorInMethod);
            advr_cdm_data_type = messages.getNotNothingString("advr_cdm_data_type", errorInMethod);
            advl_class = messages.getNotNothingString("advl_class", errorInMethod);
            advr_class = messages.getNotNothingString("advr_class", errorInMethod);
            advl_title = messages.getNotNothingString("advl_title", errorInMethod);
            advl_minLongitude = messages.getNotNothingString("advl_minLongitude", errorInMethod);
            advl_maxLongitude = messages.getNotNothingString("advl_maxLongitude", errorInMethod);
            advl_longitudeSpacing = messages.getNotNothingString("advl_longitudeSpacing", errorInMethod);
            advl_minLatitude = messages.getNotNothingString("advl_minLatitude", errorInMethod);
            advl_maxLatitude = messages.getNotNothingString("advl_maxLatitude", errorInMethod);
            advl_latitudeSpacing = messages.getNotNothingString("advl_latitudeSpacing", errorInMethod);
            advl_minAltitude = messages.getNotNothingString("advl_minAltitude", errorInMethod);
            advl_maxAltitude = messages.getNotNothingString("advl_maxAltitude", errorInMethod);
            advl_minTime = messages.getNotNothingString("advl_minTime", errorInMethod);
            advc_maxTime = messages.getNotNothingString("advc_maxTime", errorInMethod);
            advl_maxTime = messages.getNotNothingString("advl_maxTime", errorInMethod);
            advl_timeSpacing = messages.getNotNothingString("advl_timeSpacing", errorInMethod);
            advc_griddap = messages.getNotNothingString("advc_griddap", errorInMethod);
            advl_griddap = messages.getNotNothingString("advl_griddap", errorInMethod);
            advl_subset = messages.getNotNothingString("advl_subset", errorInMethod);
            advc_tabledap = messages.getNotNothingString("advc_tabledap", errorInMethod);
            advl_tabledap = messages.getNotNothingString("advl_tabledap", errorInMethod);
            advl_MakeAGraph = messages.getNotNothingString("advl_MakeAGraph", errorInMethod);
            advc_sos = messages.getNotNothingString("advc_sos", errorInMethod);
            advl_sos = messages.getNotNothingString("advl_sos", errorInMethod);
            advl_wcs = messages.getNotNothingString("advl_wcs", errorInMethod);
            advl_wms = messages.getNotNothingString("advl_wms", errorInMethod);
            advc_files = messages.getNotNothingString("advc_files", errorInMethod);
            advl_files = messages.getNotNothingString("advl_files", errorInMethod);
            advc_fgdc = messages.getNotNothingString("advc_fgdc", errorInMethod);
            advl_fgdc = messages.getNotNothingString("advl_fgdc", errorInMethod);
            advc_iso19115 = messages.getNotNothingString("advc_iso19115", errorInMethod);
            advl_iso19115 = messages.getNotNothingString("advl_iso19115", errorInMethod);
            advc_metadata = messages.getNotNothingString("advc_metadata", errorInMethod);
            advl_metadata = messages.getNotNothingString("advl_metadata", errorInMethod);
            advl_sourceUrl = messages.getNotNothingString("advl_sourceUrl", errorInMethod);
            advl_infoUrl = messages.getNotNothingString("advl_infoUrl", errorInMethod);
            advl_rss = messages.getNotNothingString("advl_rss", errorInMethod);
            advc_email = messages.getNotNothingString("advc_email", errorInMethod);
            advl_email = messages.getNotNothingString("advl_email", errorInMethod);
            advl_summary = messages.getNotNothingString("advl_summary", errorInMethod);
            advc_testOutOfDate = messages.getNotNothingString("advc_testOutOfDate", errorInMethod);
            advl_testOutOfDate = messages.getNotNothingString("advl_testOutOfDate", errorInMethod);
            advc_outOfDate = messages.getNotNothingString("advc_outOfDate", errorInMethod);
            advl_outOfDate = messages.getNotNothingString("advl_outOfDate", errorInMethod);
            advn_outOfDate = messages.getNotNothingString("advn_outOfDate", errorInMethod);
            advancedSearch = messages.getNotNothingString("advancedSearch", errorInMethod);
            advancedSearchResults = messages.getNotNothingString("advancedSearchResults", errorInMethod);
            advancedSearchDirections = messages.getNotNothingString("advancedSearchDirections", errorInMethod);
            advancedSearchTooltip = messages.getNotNothingString("advancedSearchTooltip", errorInMethod);
            advancedSearchBounds = messages.getNotNothingString("advancedSearchBounds", errorInMethod);
            advancedSearchMinLat = messages.getNotNothingString("advancedSearchMinLat", errorInMethod);
            advancedSearchMaxLat = messages.getNotNothingString("advancedSearchMaxLat", errorInMethod);
            advancedSearchMinLon = messages.getNotNothingString("advancedSearchMinLon", errorInMethod);
            advancedSearchMaxLon = messages.getNotNothingString("advancedSearchMaxLon", errorInMethod);
            advancedSearchMinMaxLon = messages.getNotNothingString("advancedSearchMinMaxLon", errorInMethod);
            advancedSearchMinTime = messages.getNotNothingString("advancedSearchMinTime", errorInMethod);
            advancedSearchMaxTime = messages.getNotNothingString("advancedSearchMaxTime", errorInMethod);
            advancedSearchClear = messages.getNotNothingString("advancedSearchClear", errorInMethod);
            advancedSearchClearHelp = messages.getNotNothingString("advancedSearchClearHelp", errorInMethod);
            advancedSearchCategoryTooltip = messages.getNotNothingString("advancedSearchCategoryTooltip",
                    errorInMethod);
            advancedSearchRangeTooltip = messages.getNotNothingString("advancedSearchRangeTooltip", errorInMethod);
            advancedSearchMapTooltip = messages.getNotNothingString("advancedSearchMapTooltip", errorInMethod);
            advancedSearchLonTooltip = messages.getNotNothingString("advancedSearchLonTooltip", errorInMethod);
            advancedSearchTimeTooltip = messages.getNotNothingString("advancedSearchTimeTooltip", errorInMethod);
            advancedSearchWithCriteria = messages.getNotNothingString("advancedSearchWithCriteria", errorInMethod);
            advancedSearchFewerCriteria = messages.getNotNothingString("advancedSearchFewerCriteria",
                    errorInMethod);
            advancedSearchNoCriteria = messages.getNotNothingString("advancedSearchNoCriteria", errorInMethod);
            autoRefresh = messages.getNotNothingString("autoRefresh", errorInMethod);
            blacklistMsg = messages.getNotNothingString("blacklistMsg", errorInMethod);
            PrimitiveArray.ArrayAddN = messages.getNotNothingString("ArrayAddN", errorInMethod);
            PrimitiveArray.ArrayAppendTables = messages.getNotNothingString("ArrayAppendTables", errorInMethod);
            PrimitiveArray.ArrayAtInsert = messages.getNotNothingString("ArrayAtInsert", errorInMethod);
            PrimitiveArray.ArrayDiff = messages.getNotNothingString("ArrayDiff", errorInMethod);
            PrimitiveArray.ArrayDifferentSize = messages.getNotNothingString("ArrayDifferentSize", errorInMethod);
            PrimitiveArray.ArrayDifferentValue = messages.getNotNothingString("ArrayDifferentValue", errorInMethod);
            PrimitiveArray.ArrayDiffString = messages.getNotNothingString("ArrayDiffString", errorInMethod);
            PrimitiveArray.ArrayMissingValue = messages.getNotNothingString("ArrayMissingValue", errorInMethod);
            PrimitiveArray.ArrayNotAscending = messages.getNotNothingString("ArrayNotAscending", errorInMethod);
            PrimitiveArray.ArrayNotDescending = messages.getNotNothingString("ArrayNotDescending", errorInMethod);
            PrimitiveArray.ArrayNotEvenlySpaced = messages.getNotNothingString("ArrayNotEvenlySpaced",
                    errorInMethod);
            PrimitiveArray.ArrayRemove = messages.getNotNothingString("ArrayRemove", errorInMethod);
            PrimitiveArray.ArraySubsetStart = messages.getNotNothingString("ArraySubsetStart", errorInMethod);
            PrimitiveArray.ArraySubsetStride = messages.getNotNothingString("ArraySubsetStride", errorInMethod);
            categoryTitleHtml = messages.getNotNothingString("categoryTitleHtml", errorInMethod);
            category1Html = messages.getNotNothingString("category1Html", errorInMethod);
            category2Html = messages.getNotNothingString("category2Html", errorInMethod);
            category3Html = messages.getNotNothingString("category3Html", errorInMethod);
            categoryPickAttribute = messages.getNotNothingString("categoryPickAttribute", errorInMethod);
            categorySearchHtml = messages.getNotNothingString("categorySearchHtml", errorInMethod);
            categorySearchDifferentHtml = messages.getNotNothingString("categorySearchDifferentHtml",
                    errorInMethod);
            categoryClickHtml = messages.getNotNothingString("categoryClickHtml", errorInMethod);
            categoryNotAnOption = messages.getNotNothingString("categoryNotAnOption", errorInMethod);
            caughtInterrupted = " " + messages.getNotNothingString("caughtInterrupted", errorInMethod);
            clickAccess = messages.getNotNothingString("clickAccess", errorInMethod);
            clickBackgroundInfo = messages.getNotNothingString("clickBackgroundInfo", errorInMethod);
            clickERDDAP = messages.getNotNothingString("clickERDDAP", errorInMethod);
            clickInfo = messages.getNotNothingString("clickInfo", errorInMethod);
            clickToSubmit = messages.getNotNothingString("clickToSubmit", errorInMethod);
            convertOceanicAtmosphericAcronyms = messages.getNotNothingString("convertOceanicAtmosphericAcronyms",
                    errorInMethod);
            convertOceanicAtmosphericAcronymsIntro = messages
                    .getNotNothingString("convertOceanicAtmosphericAcronymsIntro", errorInMethod);
            convertOceanicAtmosphericAcronymsNotes = messages
                    .getNotNothingString("convertOceanicAtmosphericAcronymsNotes", errorInMethod);
            convertOceanicAtmosphericAcronymsService = messages
                    .getNotNothingString("convertOceanicAtmosphericAcronymsService", errorInMethod);
            convertOceanicAtmosphericVariableNames = messages
                    .getNotNothingString("convertOceanicAtmosphericVariableNames", errorInMethod);
            convertOceanicAtmosphericVariableNamesIntro = messages
                    .getNotNothingString("convertOceanicAtmosphericVariableNamesIntro", errorInMethod);
            convertOceanicAtmosphericVariableNamesNotes = messages
                    .getNotNothingString("convertOceanicAtmosphericVariableNamesNotes", errorInMethod);
            convertOceanicAtmosphericVariableNamesService = messages
                    .getNotNothingString("convertOceanicAtmosphericVariableNamesService", errorInMethod);
            convertFipsCounty = messages.getNotNothingString("convertFipsCounty", errorInMethod);
            convertFipsCountyIntro = messages.getNotNothingString("convertFipsCountyIntro", errorInMethod);
            convertFipsCountyNotes = messages.getNotNothingString("convertFipsCountyNotes", errorInMethod);
            convertFipsCountyService = messages.getNotNothingString("convertFipsCountyService", errorInMethod);
            convertHtml = messages.getNotNothingString("convertHtml", errorInMethod);
            convertKeywords = messages.getNotNothingString("convertKeywords", errorInMethod);
            convertKeywordsCfTooltip = messages.getNotNothingString("convertKeywordsCfTooltip", errorInMethod);
            convertKeywordsGcmdTooltip = messages.getNotNothingString("convertKeywordsGcmdTooltip", errorInMethod);
            convertKeywordsIntro = messages.getNotNothingString("convertKeywordsIntro", errorInMethod);
            convertKeywordsNotes = messages.getNotNothingString("convertKeywordsNotes", errorInMethod);
            convertKeywordsService = messages.getNotNothingString("convertKeywordsService", errorInMethod);
            convertTime = messages.getNotNothingString("convertTime", errorInMethod);
            convertTimeBypass = messages.getNotNothingString("convertTimeBypass", errorInMethod);
            convertTimeReference = messages.getNotNothingString("convertTimeReference", errorInMethod);
            convertTimeIntro = messages.getNotNothingString("convertTimeIntro", errorInMethod);
            convertTimeNotes = messages.getNotNothingString("convertTimeNotes", errorInMethod);
            convertTimeService = messages.getNotNothingString("convertTimeService", errorInMethod);
            convertTimeNumberTooltip = messages.getNotNothingString("convertTimeNumberTooltip", errorInMethod);
            convertTimeStringTimeTooltip = messages.getNotNothingString("convertTimeStringTimeTooltip",
                    errorInMethod);
            convertTimeUnitsTooltip = messages.getNotNothingString("convertTimeUnitsTooltip", errorInMethod);
            convertTimeUnitsHelp = messages.getNotNothingString("convertTimeUnitsHelp", errorInMethod);
            convertTimeIsoFormatError = messages.getNotNothingString("convertTimeIsoFormatError", errorInMethod);
            convertTimeNoSinceError = messages.getNotNothingString("convertTimeNoSinceError", errorInMethod);
            convertTimeNumberError = messages.getNotNothingString("convertTimeNumberError", errorInMethod);
            convertTimeNumericTimeError = messages.getNotNothingString("convertTimeNumericTimeError",
                    errorInMethod);
            convertTimeParametersError = messages.getNotNothingString("convertTimeParametersError", errorInMethod);
            convertTimeStringFormatError = messages.getNotNothingString("convertTimeStringFormatError",
                    errorInMethod);
            convertTimeTwoTimeError = messages.getNotNothingString("convertTimeTwoTimeError", errorInMethod);
            convertTimeUnitsError = messages.getNotNothingString("convertTimeUnitsError", errorInMethod);
            convertUnits = messages.getNotNothingString("convertUnits", errorInMethod);
            convertUnitsComparison = messages.getNotNothingString("convertUnitsComparison", errorInMethod);
            convertUnitsFilter = messages.getNotNothingString("convertUnitsFilter", errorInMethod);
            convertUnitsIntro = messages.getNotNothingString("convertUnitsIntro", errorInMethod);
            convertUnitsNotes = messages.getNotNothingString("convertUnitsNotes", errorInMethod);
            convertUnitsService = messages.getNotNothingString("convertUnitsService", errorInMethod);
            cookiesHelp = messages.getNotNothingString("cookiesHelp", errorInMethod);
            daf = messages.getNotNothingString("daf", errorInMethod);
            dafGridBypassTooltip = messages.getNotNothingString("dafGridBypassTooltip", errorInMethod);
            dafGridTooltip = messages.getNotNothingString("dafGridTooltip", errorInMethod);
            dafTableBypassTooltip = messages.getNotNothingString("dafTableBypassTooltip", errorInMethod);
            dafTableTooltip = messages.getNotNothingString("dafTableTooltip", errorInMethod);
            dasTitle = messages.getNotNothingString("dasTitle", errorInMethod);
            dataAccessNotAllowed = messages.getNotNothingString("dataAccessNotAllowed", errorInMethod);
            databaseUnableToConnect = messages.getNotNothingString("databaseUnableToConnect", errorInMethod);
            disabled = messages.getNotNothingString("disabled", errorInMethod);
            distinctValuesTooltip = messages.getNotNothingString("distinctValuesTooltip", errorInMethod);
            doWithGraphs = messages.getNotNothingString("doWithGraphs", errorInMethod);

            dtAccessible = messages.getNotNothingString("dtAccessible", errorInMethod);
            dtAccessibleYes = messages.getNotNothingString("dtAccessibleYes", errorInMethod);
            dtAccessibleGraphs = messages.getNotNothingString("dtAccessibleGraphs", errorInMethod);
            dtAccessibleNo = messages.getNotNothingString("dtAccessibleNo", errorInMethod);
            dtAccessibleLogIn = messages.getNotNothingString("dtAccessibleLogIn", errorInMethod);
            dtLogIn = messages.getNotNothingString("dtLogIn", errorInMethod);
            dtDAF1 = messages.getNotNothingString("dtDAF1", errorInMethod);
            dtDAF2 = messages.getNotNothingString("dtDAF2", errorInMethod);
            dtFiles = messages.getNotNothingString("dtFiles", errorInMethod);
            dtMAG = messages.getNotNothingString("dtMAG", errorInMethod);
            dtSOS = messages.getNotNothingString("dtSOS", errorInMethod);
            dtSubset = messages.getNotNothingString("dtSubset", errorInMethod);
            dtWCS = messages.getNotNothingString("dtWCS", errorInMethod);
            dtWMS = messages.getNotNothingString("dtWMS", errorInMethod);

            EDDDatasetID = messages.getNotNothingString("EDDDatasetID", errorInMethod);
            EDDFgdc = messages.getNotNothingString("EDDFgdc", errorInMethod);
            EDDFgdcMetadata = messages.getNotNothingString("EDDFgdcMetadata", errorInMethod);
            EDDFiles = messages.getNotNothingString("EDDFiles", errorInMethod);
            EDDIso19115 = messages.getNotNothingString("EDDIso19115", errorInMethod);
            EDDIso19115Metadata = messages.getNotNothingString("EDDIso19115Metadata", errorInMethod);
            EDDMetadata = messages.getNotNothingString("EDDMetadata", errorInMethod);
            EDDBackground = messages.getNotNothingString("EDDBackground", errorInMethod);
            EDDClickOnSubmitHtml = messages.getNotNothingString("EDDClickOnSubmitHtml", errorInMethod);
            EDDInformation = messages.getNotNothingString("EDDInformation", errorInMethod);
            EDDInstitution = messages.getNotNothingString("EDDInstitution", errorInMethod);
            EDDSummary = messages.getNotNothingString("EDDSummary", errorInMethod);
            EDDDatasetTitle = messages.getNotNothingString("EDDDatasetTitle", errorInMethod);
            EDDDownloadData = messages.getNotNothingString("EDDDownloadData", errorInMethod);
            EDDMakeAGraph = messages.getNotNothingString("EDDMakeAGraph", errorInMethod);
            EDDMakeAMap = messages.getNotNothingString("EDDMakeAMap", errorInMethod);
            EDDFileType = messages.getNotNothingString("EDDFileType", errorInMethod);
            EDDFileTypeInformation = messages.getNotNothingString("EDDFileTypeInformation", errorInMethod);
            EDDSelectFileType = messages.getNotNothingString("EDDSelectFileType", errorInMethod);
            EDDMinimum = messages.getNotNothingString("EDDMinimum", errorInMethod);
            EDDMaximum = messages.getNotNothingString("EDDMaximum", errorInMethod);
            EDDConstraint = messages.getNotNothingString("EDDConstraint", errorInMethod);

            EDDChangedWasnt = messages.getNotNothingString("EDDChangedWasnt", errorInMethod);
            EDDChangedDifferentNVar = messages.getNotNothingString("EDDChangedDifferentNVar", errorInMethod);
            EDDChanged2Different = messages.getNotNothingString("EDDChanged2Different", errorInMethod);
            EDDChanged1Different = messages.getNotNothingString("EDDChanged1Different", errorInMethod);
            EDDChangedCGADifferent = messages.getNotNothingString("EDDChangedCGADifferent", errorInMethod);
            EDDChangedAxesDifferentNVar = messages.getNotNothingString("EDDChangedAxesDifferentNVar",
                    errorInMethod);
            EDDChangedAxes2Different = messages.getNotNothingString("EDDChangedAxes2Different", errorInMethod);
            EDDChangedAxes1Different = messages.getNotNothingString("EDDChangedAxes1Different", errorInMethod);
            EDDChangedNoValue = messages.getNotNothingString("EDDChangedNoValue", errorInMethod);
            EDDChangedTableToGrid = messages.getNotNothingString("EDDChangedTableToGrid", errorInMethod);

            EDDSimilarDifferentNVar = messages.getNotNothingString("EDDSimilarDifferentNVar", errorInMethod);
            EDDSimilarDifferent = messages.getNotNothingString("EDDSimilarDifferent", errorInMethod);

            EDDGridDownloadTooltip = messages.getNotNothingString("EDDGridDownloadTooltip", errorInMethod);
            EDDGridDapDescription = messages.getNotNothingString("EDDGridDapDescription", errorInMethod);
            EDDGridDapLongDescription = messages.getNotNothingString("EDDGridDapLongDescription", errorInMethod);
            EDDGridDownloadDataTooltip = messages.getNotNothingString("EDDGridDownloadDataTooltip", errorInMethod);
            EDDGridDimension = messages.getNotNothingString("EDDGridDimension", errorInMethod);
            EDDGridDimensionRanges = messages.getNotNothingString("EDDGridDimensionRanges", errorInMethod);
            EDDGridFirst = messages.getNotNothingString("EDDGridFirst", errorInMethod);
            EDDGridLast = messages.getNotNothingString("EDDGridLast", errorInMethod);
            EDDGridStart = messages.getNotNothingString("EDDGridStart", errorInMethod);
            EDDGridStop = messages.getNotNothingString("EDDGridStop", errorInMethod);
            EDDGridStartStopTooltip = messages.getNotNothingString("EDDGridStartStopTooltip", errorInMethod);
            EDDGridStride = messages.getNotNothingString("EDDGridStride", errorInMethod);
            EDDGridNValues = messages.getNotNothingString("EDDGridNValues", errorInMethod);
            EDDGridNValuesHtml = messages.getNotNothingString("EDDGridNValuesHtml", errorInMethod);
            EDDGridSpacing = messages.getNotNothingString("EDDGridSpacing", errorInMethod);
            EDDGridJustOneValue = messages.getNotNothingString("EDDGridJustOneValue", errorInMethod);
            EDDGridEven = messages.getNotNothingString("EDDGridEven", errorInMethod);
            EDDGridUneven = messages.getNotNothingString("EDDGridUneven", errorInMethod);
            EDDGridDimensionTooltip = messages.getNotNothingString("EDDGridDimensionTooltip", errorInMethod);
            EDDGridDimensionFirstTooltip = messages.getNotNothingString("EDDGridDimensionFirstTooltip",
                    errorInMethod);
            EDDGridDimensionLastTooltip = messages.getNotNothingString("EDDGridDimensionLastTooltip",
                    errorInMethod);
            EDDGridVarHasDimTooltip = messages.getNotNothingString("EDDGridVarHasDimTooltip", errorInMethod);
            EDDGridSSSTooltip = messages.getNotNothingString("EDDGridSSSTooltip", errorInMethod);
            EDDGridStartTooltip = messages.getNotNothingString("EDDGridStartTooltip", errorInMethod);
            EDDGridStopTooltip = messages.getNotNothingString("EDDGridStopTooltip", errorInMethod);
            EDDGridStrideTooltip = messages.getNotNothingString("EDDGridStrideTooltip", errorInMethod);
            EDDGridSpacingTooltip = messages.getNotNothingString("EDDGridSpacingTooltip", errorInMethod);
            EDDGridGridVariableHtml = messages.getNotNothingString("EDDGridGridVariableHtml", errorInMethod);

            //default EDDGrid...Example
            EDDGridErddapUrlExample = messages.getNotNothingString("EDDGridErddapUrlExample", errorInMethod);
            EDDGridIdExample = messages.getNotNothingString("EDDGridIdExample", errorInMethod);
            EDDGridDimensionExample = messages.getNotNothingString("EDDGridDimensionExample", errorInMethod);
            EDDGridNoHyperExample = messages.getNotNothingString("EDDGridNoHyperExample", errorInMethod);
            EDDGridDimNamesExample = messages.getNotNothingString("EDDGridDimNamesExample", errorInMethod);
            EDDGridDataTimeExample = messages.getNotNothingString("EDDGridDataTimeExample", errorInMethod);
            EDDGridDataValueExample = messages.getNotNothingString("EDDGridDataValueExample", errorInMethod);
            EDDGridDataIndexExample = messages.getNotNothingString("EDDGridDataIndexExample", errorInMethod);
            EDDGridGraphExample = messages.getNotNothingString("EDDGridGraphExample", errorInMethod);
            EDDGridMapExample = messages.getNotNothingString("EDDGridMapExample", errorInMethod);
            EDDGridMatlabPlotExample = messages.getNotNothingString("EDDGridMatlabPlotExample", errorInMethod);

            //admin provides EDDGrid...Example
            EDDGridErddapUrlExample = setup.getString("EDDGridErddapUrlExample", EDDGridErddapUrlExample);
            EDDGridIdExample = setup.getString("EDDGridIdExample", EDDGridIdExample);
            EDDGridDimensionExample = setup.getString("EDDGridDimensionExample", EDDGridDimensionExample);
            EDDGridNoHyperExample = setup.getString("EDDGridNoHyperExample", EDDGridNoHyperExample);
            EDDGridDimNamesExample = setup.getString("EDDGridDimNamesExample", EDDGridDimNamesExample);
            EDDGridDataIndexExample = setup.getString("EDDGridDataIndexExample", EDDGridDataIndexExample);
            EDDGridDataValueExample = setup.getString("EDDGridDataValueExample", EDDGridDataValueExample);
            EDDGridDataTimeExample = setup.getString("EDDGridDataTimeExample", EDDGridDataTimeExample);
            EDDGridGraphExample = setup.getString("EDDGridGraphExample", EDDGridGraphExample);
            EDDGridMapExample = setup.getString("EDDGridMapExample", EDDGridMapExample);
            EDDGridMatlabPlotExample = setup.getString("EDDGridMatlabPlotExample", EDDGridMatlabPlotExample);

            //variants encoded to be Html Examples
            EDDGridDimensionExampleHE = XML.encodeAsHTML(EDDGridDimensionExample);
            EDDGridDataIndexExampleHE = XML.encodeAsHTML(EDDGridDataIndexExample);
            EDDGridDataValueExampleHE = XML.encodeAsHTML(EDDGridDataValueExample);
            EDDGridDataTimeExampleHE = XML.encodeAsHTML(EDDGridDataTimeExample);
            EDDGridGraphExampleHE = XML.encodeAsHTML(EDDGridGraphExample);
            EDDGridMapExampleHE = XML.encodeAsHTML(EDDGridMapExample);

            //variants encoded to be Html Attributes
            EDDGridDimensionExampleHA = XML.encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDGridDimensionExample));
            EDDGridDataIndexExampleHA = XML.encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDGridDataIndexExample));
            EDDGridDataValueExampleHA = XML.encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDGridDataValueExample));
            EDDGridDataTimeExampleHA = XML.encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDGridDataTimeExample));
            EDDGridGraphExampleHA = XML.encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDGridGraphExample));
            EDDGridMapExampleHA = XML.encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDGridMapExample));

            EDDTableConstraints = messages.getNotNothingString("EDDTableConstraints", errorInMethod);
            EDDTableDapDescription = messages.getNotNothingString("EDDTableDapDescription", errorInMethod);
            EDDTableDapLongDescription = messages.getNotNothingString("EDDTableDapLongDescription", errorInMethod);
            EDDTableDownloadDataTooltip = messages.getNotNothingString("EDDTableDownloadDataTooltip",
                    errorInMethod);
            EDDTableTabularDatasetTooltip = messages.getNotNothingString("EDDTableTabularDatasetTooltip",
                    errorInMethod);
            EDDTableVariable = messages.getNotNothingString("EDDTableVariable", errorInMethod);
            EDDTableCheckAll = messages.getNotNothingString("EDDTableCheckAll", errorInMethod);
            EDDTableCheckAllTooltip = messages.getNotNothingString("EDDTableCheckAllTooltip", errorInMethod);
            EDDTableUncheckAll = messages.getNotNothingString("EDDTableUncheckAll", errorInMethod);
            EDDTableUncheckAllTooltip = messages.getNotNothingString("EDDTableUncheckAllTooltip", errorInMethod);
            EDDTableMinimumTooltip = messages.getNotNothingString("EDDTableMinimumTooltip", errorInMethod);
            EDDTableMaximumTooltip = messages.getNotNothingString("EDDTableMaximumTooltip", errorInMethod);
            EDDTableCheckTheVariables = messages.getNotNothingString("EDDTableCheckTheVariables", errorInMethod);
            EDDTableSelectAnOperator = messages.getNotNothingString("EDDTableSelectAnOperator", errorInMethod);
            EDDTableOptConstraint1Html = messages.getNotNothingString("EDDTableOptConstraint1Html", errorInMethod);
            EDDTableOptConstraint2Html = messages.getNotNothingString("EDDTableOptConstraint2Html", errorInMethod);
            EDDTableOptConstraintVar = messages.getNotNothingString("EDDTableOptConstraintVar", errorInMethod);
            EDDTableNumericConstraintTooltip = messages.getNotNothingString("EDDTableNumericConstraintTooltip",
                    errorInMethod);
            EDDTableStringConstraintTooltip = messages.getNotNothingString("EDDTableStringConstraintTooltip",
                    errorInMethod);
            EDDTableTimeConstraintTooltip = messages.getNotNothingString("EDDTableTimeConstraintTooltip",
                    errorInMethod);
            EDDTableConstraintTooltip = messages.getNotNothingString("EDDTableConstraintTooltip", errorInMethod);
            EDDTableSelectConstraintTooltip = messages.getNotNothingString("EDDTableSelectConstraintTooltip",
                    errorInMethod);

            //default EDDGrid...Example
            EDDTableErddapUrlExample = messages.getNotNothingString("EDDTableErddapUrlExample", errorInMethod);
            EDDTableIdExample = messages.getNotNothingString("EDDTableIdExample", errorInMethod);
            EDDTableVariablesExample = messages.getNotNothingString("EDDTableVariablesExample", errorInMethod);
            EDDTableConstraintsExample = messages.getNotNothingString("EDDTableConstraintsExample", errorInMethod);
            EDDTableDataValueExample = messages.getNotNothingString("EDDTableDataValueExample", errorInMethod);
            EDDTableDataTimeExample = messages.getNotNothingString("EDDTableDataTimeExample", errorInMethod);
            EDDTableGraphExample = messages.getNotNothingString("EDDTableGraphExample", errorInMethod);
            EDDTableMapExample = messages.getNotNothingString("EDDTableMapExample", errorInMethod);
            EDDTableMatlabPlotExample = messages.getNotNothingString("EDDTableMatlabPlotExample", errorInMethod);

            //admin provides EDDGrid...Example
            EDDTableErddapUrlExample = setup.getString("EDDTableErddapUrlExample", EDDTableErddapUrlExample);
            EDDTableIdExample = setup.getString("EDDTableIdExample", EDDTableIdExample);
            EDDTableVariablesExample = setup.getString("EDDTableVariablesExample", EDDTableVariablesExample);
            EDDTableConstraintsExample = setup.getString("EDDTableConstraintsExample", EDDTableConstraintsExample);
            EDDTableDataValueExample = setup.getString("EDDTableDataValueExample", EDDTableDataValueExample);
            EDDTableDataTimeExample = setup.getString("EDDTableDataTimeExample", EDDTableDataTimeExample);
            EDDTableGraphExample = setup.getString("EDDTableGraphExample", EDDTableGraphExample);
            EDDTableMapExample = setup.getString("EDDTableMapExample", EDDTableMapExample);
            EDDTableMatlabPlotExample = setup.getString("EDDTableMatlabPlotExample", EDDTableMatlabPlotExample);

            //variants encoded to be Html Examples
            EDDTableConstraintsExampleHE = XML.encodeAsHTML(EDDTableConstraintsExample);
            EDDTableDataTimeExampleHE = XML.encodeAsHTML(EDDTableDataTimeExample);
            EDDTableDataValueExampleHE = XML.encodeAsHTML(EDDTableDataValueExample);
            EDDTableGraphExampleHE = XML.encodeAsHTML(EDDTableGraphExample);
            EDDTableMapExampleHE = XML.encodeAsHTML(EDDTableMapExample);

            //variants encoded to be Html Attributes
            EDDTableConstraintsExampleHA = XML
                    .encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDTableConstraintsExample));
            EDDTableDataTimeExampleHA = XML.encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDTableDataTimeExample));
            EDDTableDataValueExampleHA = XML
                    .encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDTableDataValueExample));
            EDDTableGraphExampleHA = XML.encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDTableGraphExample));
            EDDTableMapExampleHA = XML.encodeAsHTMLAttribute(SSR.psuedoPercentEncode(EDDTableMapExample));

            errorTitle = messages.getNotNothingString("errorTitle", errorInMethod);
            errorRequestUrl = messages.getNotNothingString("errorRequestUrl", errorInMethod);
            errorRequestQuery = messages.getNotNothingString("errorRequestQuery", errorInMethod);
            errorTheError = messages.getNotNothingString("errorTheError", errorInMethod);
            errorCopyFrom = messages.getNotNothingString("errorCopyFrom", errorInMethod);
            errorFileNotFound = messages.getNotNothingString("errorFileNotFound", errorInMethod);
            errorFileNotFoundImage = messages.getNotNothingString("errorFileNotFoundImage", errorInMethod);
            errorInternal = messages.getNotNothingString("errorInternal", errorInMethod) + " ";
            errorJsonpFunctionName = messages.getNotNothingString("errorJsonpFunctionName", errorInMethod);
            errorJsonpNotAllowed = messages.getNotNothingString("errorJsonpNotAllowed", errorInMethod);
            errorMoreThan2GB = messages.getNotNothingString("errorMoreThan2GB", errorInMethod);
            errorNotFound = messages.getNotNothingString("errorNotFound", errorInMethod);
            errorNotFoundIn = messages.getNotNothingString("errorNotFoundIn", errorInMethod);
            errorOdvLLTGrid = messages.getNotNothingString("errorOdvLLTGrid", errorInMethod);
            errorOdvLLTTable = messages.getNotNothingString("errorOdvLLTTable", errorInMethod);
            errorOnWebPage = messages.getNotNothingString("errorOnWebPage", errorInMethod);
            errorXWasntSpecified = messages.getNotNothingString("errorXWasntSpecified", errorInMethod);
            HtmlWidgets.errorXWasntSpecified = errorXWasntSpecified;
            errorXWasTooLong = messages.getNotNothingString("errorXWasTooLong", errorInMethod);
            HtmlWidgets.errorXWasTooLong = errorXWasTooLong;
            externalLink = " " + messages.getNotNothingString("externalLink", errorInMethod);
            externalWebSite = messages.getNotNothingString("externalWebSite", errorInMethod);
            fileHelp_asc = messages.getNotNothingString("fileHelp_asc", errorInMethod);
            fileHelp_csv = messages.getNotNothingString("fileHelp_csv", errorInMethod);
            fileHelp_csvp = messages.getNotNothingString("fileHelp_csvp", errorInMethod);
            fileHelp_csv0 = messages.getNotNothingString("fileHelp_csv0", errorInMethod);
            fileHelp_das = messages.getNotNothingString("fileHelp_das", errorInMethod);
            fileHelp_dds = messages.getNotNothingString("fileHelp_dds", errorInMethod);
            fileHelp_dods = messages.getNotNothingString("fileHelp_dods", errorInMethod);
            fileHelpGrid_esriAscii = messages.getNotNothingString("fileHelpGrid_esriAscii", errorInMethod);
            fileHelpTable_esriCsv = messages.getNotNothingString("fileHelpTable_esriCsv", errorInMethod);
            fileHelp_fgdc = messages.getNotNothingString("fileHelp_fgdc", errorInMethod);
            fileHelp_geoJson = messages.getNotNothingString("fileHelp_geoJson", errorInMethod);
            fileHelp_graph = messages.getNotNothingString("fileHelp_graph", errorInMethod);
            fileHelpGrid_help = messages.getNotNothingString("fileHelpGrid_help", errorInMethod);
            fileHelpTable_help = messages.getNotNothingString("fileHelpTable_help", errorInMethod);
            fileHelp_html = messages.getNotNothingString("fileHelp_html", errorInMethod);
            fileHelp_htmlTable = messages.getNotNothingString("fileHelp_htmlTable", errorInMethod);
            fileHelp_iso19115 = messages.getNotNothingString("fileHelp_iso19115", errorInMethod);
            fileHelp_itxGrid = messages.getNotNothingString("fileHelp_itxGrid", errorInMethod);
            fileHelp_itxTable = messages.getNotNothingString("fileHelp_itxTable", errorInMethod);
            fileHelp_json = messages.getNotNothingString("fileHelp_json", errorInMethod);
            fileHelp_jsonlCSV = messages.getNotNothingString("fileHelp_jsonlCSV", errorInMethod);
            fileHelp_jsonlKVP = messages.getNotNothingString("fileHelp_jsonlKVP", errorInMethod);
            fileHelp_mat = messages.getNotNothingString("fileHelp_mat", errorInMethod);
            fileHelpGrid_nc3 = messages.getNotNothingString("fileHelpGrid_nc3", errorInMethod);
            fileHelpGrid_nc4 = messages.getNotNothingString("fileHelpGrid_nc4", errorInMethod);
            fileHelpTable_nc3 = messages.getNotNothingString("fileHelpTable_nc3", errorInMethod);
            fileHelpTable_nc4 = messages.getNotNothingString("fileHelpTable_nc4", errorInMethod);
            fileHelp_nc3Header = messages.getNotNothingString("fileHelp_nc3Header", errorInMethod);
            fileHelp_nc4Header = messages.getNotNothingString("fileHelp_nc4Header", errorInMethod);
            fileHelp_nccsv = messages.getNotNothingString("fileHelp_nccsv", errorInMethod);
            fileHelp_nccsvMetadata = messages.getNotNothingString("fileHelp_nccsvMetadata", errorInMethod);
            fileHelp_ncCF = messages.getNotNothingString("fileHelp_ncCF", errorInMethod);
            fileHelp_ncCFHeader = messages.getNotNothingString("fileHelp_ncCFHeader", errorInMethod);
            fileHelp_ncCFMA = messages.getNotNothingString("fileHelp_ncCFMA", errorInMethod);
            fileHelp_ncCFMAHeader = messages.getNotNothingString("fileHelp_ncCFMAHeader", errorInMethod);
            fileHelp_ncml = messages.getNotNothingString("fileHelp_ncml", errorInMethod);
            fileHelp_ncoJson = messages.getNotNothingString("fileHelp_ncoJson", errorInMethod);
            fileHelpGrid_odvTxt = messages.getNotNothingString("fileHelpGrid_odvTxt", errorInMethod);
            fileHelpTable_odvTxt = messages.getNotNothingString("fileHelpTable_odvTxt", errorInMethod);
            fileHelp_subset = messages.getNotNothingString("fileHelp_subset", errorInMethod);
            fileHelp_timeGaps = messages.getNotNothingString("fileHelp_timeGaps", errorInMethod);
            fileHelp_tsv = messages.getNotNothingString("fileHelp_tsv", errorInMethod);
            fileHelp_tsvp = messages.getNotNothingString("fileHelp_tsvp", errorInMethod);
            fileHelp_tsv0 = messages.getNotNothingString("fileHelp_tsv0", errorInMethod);
            fileHelp_wav = messages.getNotNothingString("fileHelp_wav", errorInMethod);
            fileHelp_xhtml = messages.getNotNothingString("fileHelp_xhtml", errorInMethod);
            fileHelp_geotif = messages.getNotNothingString("fileHelp_geotif", errorInMethod);
            fileHelpGrid_kml = messages.getNotNothingString("fileHelpGrid_kml", errorInMethod);
            fileHelpTable_kml = messages.getNotNothingString("fileHelpTable_kml", errorInMethod);
            fileHelp_smallPdf = messages.getNotNothingString("fileHelp_smallPdf", errorInMethod);
            fileHelp_pdf = messages.getNotNothingString("fileHelp_pdf", errorInMethod);
            fileHelp_largePdf = messages.getNotNothingString("fileHelp_largePdf", errorInMethod);
            fileHelp_smallPng = messages.getNotNothingString("fileHelp_smallPng", errorInMethod);
            fileHelp_png = messages.getNotNothingString("fileHelp_png", errorInMethod);
            fileHelp_largePng = messages.getNotNothingString("fileHelp_largePng", errorInMethod);
            fileHelp_transparentPng = messages.getNotNothingString("fileHelp_transparentPng", errorInMethod);
            filesDescription = messages.getNotNothingString("filesDescription", errorInMethod);
            filesDocumentation = messages.getNotNothingString("filesDocumentation", errorInMethod);
            filesSort = messages.getNotNothingString("filesSort", errorInMethod);
            filesWarning = messages.getNotNothingString("filesWarning", errorInMethod);
            functions = messages.getNotNothingString("functions", errorInMethod);
            functionTooltip = messages.getNotNothingString("functionTooltip", errorInMethod);
            functionDistinctCheck = messages.getNotNothingString("functionDistinctCheck", errorInMethod);
            functionDistinctTooltip = messages.getNotNothingString("functionDistinctTooltip", errorInMethod);
            functionOrderByExtra = messages.getNotNothingString("functionOrderByExtra", errorInMethod);
            functionOrderByTooltip = messages.getNotNothingString("functionOrderByTooltip", errorInMethod);
            functionOrderBySort = messages.getNotNothingString("functionOrderBySort", errorInMethod);
            functionOrderBySort1 = messages.getNotNothingString("functionOrderBySort1", errorInMethod);
            functionOrderBySort2 = messages.getNotNothingString("functionOrderBySort2", errorInMethod);
            functionOrderBySort3 = messages.getNotNothingString("functionOrderBySort3", errorInMethod);
            functionOrderBySort4 = messages.getNotNothingString("functionOrderBySort4", errorInMethod);
            functionOrderBySortLeast = messages.getNotNothingString("functionOrderBySortLeast", errorInMethod);
            functionOrderBySortRowMax = messages.getNotNothingString("functionOrderBySortRowMax", errorInMethod);
            generatedAt = messages.getNotNothingString("generatedAt", errorInMethod);
            geoServicesDescription = messages.getNotNothingString("geoServicesDescription", errorInMethod);
            getStartedHtml = messages.getNotNothingString("getStartedHtml", errorInMethod);
            TableWriterHtmlTable.htmlTableMaxMB = messages.getInt("htmlTableMaxMB",
                    TableWriterHtmlTable.htmlTableMaxMB);
            htmlTableMaxMessage = messages.getNotNothingString("htmlTableMaxMessage", errorInMethod);
            imageDataCourtesyOf = messages.getNotNothingString("imageDataCourtesyOf", errorInMethod);
            imageWidths = String2
                    .toIntArray(String2.split(messages.getNotNothingString("imageWidths", errorInMethod), ','));
            imageHeights = String2
                    .toIntArray(String2.split(messages.getNotNothingString("imageHeights", errorInMethod), ','));
            indexViewAll = messages.getNotNothingString("indexViewAll", errorInMethod);
            indexSearchWith = messages.getNotNothingString("indexSearchWith", errorInMethod);
            indexDevelopersSearch = messages.getNotNothingString("indexDevelopersSearch", errorInMethod);
            indexProtocol = messages.getNotNothingString("indexProtocol", errorInMethod);
            indexDescription = messages.getNotNothingString("indexDescription", errorInMethod);
            indexDatasets = messages.getNotNothingString("indexDatasets", errorInMethod);
            indexDocumentation = messages.getNotNothingString("indexDocumentation", errorInMethod);
            indexRESTfulSearch = messages.getNotNothingString("indexRESTfulSearch", errorInMethod);
            indexAllDatasetsSearch = messages.getNotNothingString("indexAllDatasetsSearch", errorInMethod);
            indexOpenSearch = messages.getNotNothingString("indexOpenSearch", errorInMethod);
            indexServices = messages.getNotNothingString("indexServices", errorInMethod);
            indexDescribeServices = messages.getNotNothingString("indexDescribeServices", errorInMethod);
            indexMetadata = messages.getNotNothingString("indexMetadata", errorInMethod);
            indexWAF1 = messages.getNotNothingString("indexWAF1", errorInMethod);
            indexWAF2 = messages.getNotNothingString("indexWAF2", errorInMethod);
            indexConverters = messages.getNotNothingString("indexConverters", errorInMethod);
            indexDescribeConverters = messages.getNotNothingString("indexDescribeConverters", errorInMethod);
            infoAboutFrom = messages.getNotNothingString("infoAboutFrom", errorInMethod);
            infoTableTitleHtml = messages.getNotNothingString("infoTableTitleHtml", errorInMethod);
            infoRequestForm = messages.getNotNothingString("infoRequestForm", errorInMethod);
            inotifyFix = messages.getNotNothingString("inotifyFix", errorInMethod);
            justGenerateAndView = messages.getNotNothingString("justGenerateAndView", errorInMethod);
            justGenerateAndViewTooltip = messages.getNotNothingString("justGenerateAndViewTooltip", errorInMethod);
            justGenerateAndViewUrl = messages.getNotNothingString("justGenerateAndViewUrl", errorInMethod);
            justGenerateAndViewGraphUrlTooltip = messages.getNotNothingString("justGenerateAndViewGraphUrlTooltip",
                    errorInMethod);
            license = messages.getNotNothingString("license", errorInMethod);
            listAll = messages.getNotNothingString("listAll", errorInMethod);
            listOfDatasets = messages.getNotNothingString("listOfDatasets", errorInMethod);
            LogIn = messages.getNotNothingString("LogIn", errorInMethod);
            login = messages.getNotNothingString("login", errorInMethod);
            loginAttemptBlocked = messages.getNotNothingString("loginAttemptBlocked", errorInMethod);
            loginDescribeCustom = messages.getNotNothingString("loginDescribeCustom", errorInMethod);
            loginDescribeEmail = messages.getNotNothingString("loginDescribeEmail", errorInMethod);
            loginDescribeGoogle = messages.getNotNothingString("loginDescribeGoogle", errorInMethod);
            loginDescribeOpenID = messages.getNotNothingString("loginDescribeOpenID", errorInMethod);
            loginCanNot = messages.getNotNothingString("loginCanNot", errorInMethod);
            loginAreNot = messages.getNotNothingString("loginAreNot", errorInMethod);
            loginToLogIn = messages.getNotNothingString("loginToLogIn", errorInMethod);
            loginEmailAddress = messages.getNotNothingString("loginEmailAddress", errorInMethod);
            loginYourEmailAddress = messages.getNotNothingString("loginYourEmailAddress", errorInMethod);
            loginUserName = messages.getNotNothingString("loginUserName", errorInMethod);
            loginPassword = messages.getNotNothingString("loginPassword", errorInMethod);
            loginUserNameAndPassword = messages.getNotNothingString("loginUserNameAndPassword", errorInMethod);
            loginGoogleSignIn = messages.getNotNothingString("loginGoogleSignIn", errorInMethod);
            loginGoogleErddap = messages.getNotNothingString("loginGoogleErddap", errorInMethod);
            loginOpenID = messages.getNotNothingString("loginOpenID", errorInMethod);
            loginOpenIDOr = messages.getNotNothingString("loginOpenIDOr", errorInMethod);
            loginOpenIDCreate = messages.getNotNothingString("loginOpenIDCreate", errorInMethod);
            loginOpenIDFree = messages.getNotNothingString("loginOpenIDFree", errorInMethod);
            loginOpenIDSame = messages.getNotNothingString("loginOpenIDSame", errorInMethod);
            loginAs = messages.getNotNothingString("loginAs", errorInMethod);
            loginFailed = messages.getNotNothingString("loginFailed", errorInMethod);
            loginSucceeded = messages.getNotNothingString("loginSucceeded", errorInMethod);
            loginInvalid = messages.getNotNothingString("loginInvalid", errorInMethod);
            loginNot = messages.getNotNothingString("loginNot", errorInMethod);
            loginBack = messages.getNotNothingString("loginBack", errorInMethod);
            loginProblemExact = messages.getNotNothingString("loginProblemExact", errorInMethod);
            loginProblemExpire = messages.getNotNothingString("loginProblemExpire", errorInMethod);
            loginProblemGoogleAgain = messages.getNotNothingString("loginProblemGoogleAgain", errorInMethod);
            loginProblemSameBrowser = messages.getNotNothingString("loginProblemSameBrowser", errorInMethod);
            loginProblem3Times = messages.getNotNothingString("loginProblem3Times", errorInMethod);
            loginProblems = messages.getNotNothingString("loginProblems", errorInMethod);
            loginProblemsAfter = messages.getNotNothingString("loginProblemsAfter", errorInMethod);
            loginPublicAccess = messages.getNotNothingString("loginPublicAccess", errorInMethod);
            LogOut = messages.getNotNothingString("LogOut", errorInMethod);
            logout = messages.getNotNothingString("logout", errorInMethod);
            logoutOpenID = messages.getNotNothingString("logoutOpenID", errorInMethod);
            logoutSuccess = messages.getNotNothingString("logoutSuccess", errorInMethod);
            mag = messages.getNotNothingString("mag", errorInMethod);
            magAxisX = messages.getNotNothingString("magAxisX", errorInMethod);
            magAxisY = messages.getNotNothingString("magAxisY", errorInMethod);
            magAxisColor = messages.getNotNothingString("magAxisColor", errorInMethod);
            magAxisStickX = messages.getNotNothingString("magAxisStickX", errorInMethod);
            magAxisStickY = messages.getNotNothingString("magAxisStickY", errorInMethod);
            magAxisVectorX = messages.getNotNothingString("magAxisVectorX", errorInMethod);
            magAxisVectorY = messages.getNotNothingString("magAxisVectorY", errorInMethod);
            magAxisHelpGraphX = messages.getNotNothingString("magAxisHelpGraphX", errorInMethod);
            magAxisHelpGraphY = messages.getNotNothingString("magAxisHelpGraphY", errorInMethod);
            magAxisHelpMarkerColor = messages.getNotNothingString("magAxisHelpMarkerColor", errorInMethod);
            magAxisHelpSurfaceColor = messages.getNotNothingString("magAxisHelpSurfaceColor", errorInMethod);
            magAxisHelpStickX = messages.getNotNothingString("magAxisHelpStickX", errorInMethod);
            magAxisHelpStickY = messages.getNotNothingString("magAxisHelpStickY", errorInMethod);
            magAxisHelpMapX = messages.getNotNothingString("magAxisHelpMapX", errorInMethod);
            magAxisHelpMapY = messages.getNotNothingString("magAxisHelpMapY", errorInMethod);
            magAxisHelpVectorX = messages.getNotNothingString("magAxisHelpVectorX", errorInMethod);
            magAxisHelpVectorY = messages.getNotNothingString("magAxisHelpVectorY", errorInMethod);
            magAxisVarHelp = messages.getNotNothingString("magAxisVarHelp", errorInMethod);
            magAxisVarHelpGrid = messages.getNotNothingString("magAxisVarHelpGrid", errorInMethod);
            magConstraintHelp = messages.getNotNothingString("magConstraintHelp", errorInMethod);
            magDocumentation = messages.getNotNothingString("magDocumentation", errorInMethod);
            magDownload = messages.getNotNothingString("magDownload", errorInMethod);
            magDownloadTooltip = messages.getNotNothingString("magDownloadTooltip", errorInMethod);
            magFileType = messages.getNotNothingString("magFileType", errorInMethod);
            magGraphType = messages.getNotNothingString("magGraphType", errorInMethod);
            magGraphTypeTooltipGrid = messages.getNotNothingString("magGraphTypeTooltipGrid", errorInMethod);
            magGraphTypeTooltipTable = messages.getNotNothingString("magGraphTypeTooltipTable", errorInMethod);
            magGS = messages.getNotNothingString("magGS", errorInMethod);
            magGSMarkerType = messages.getNotNothingString("magGSMarkerType", errorInMethod);
            magGSSize = messages.getNotNothingString("magGSSize", errorInMethod);
            magGSColor = messages.getNotNothingString("magGSColor", errorInMethod);
            magGSColorBar = messages.getNotNothingString("magGSColorBar", errorInMethod);
            magGSColorBarTooltip = messages.getNotNothingString("magGSColorBarTooltip", errorInMethod);
            magGSContinuity = messages.getNotNothingString("magGSContinuity", errorInMethod);
            magGSContinuityTooltip = messages.getNotNothingString("magGSContinuityTooltip", errorInMethod);
            magGSScale = messages.getNotNothingString("magGSScale", errorInMethod);
            magGSScaleTooltip = messages.getNotNothingString("magGSScaleTooltip", errorInMethod);
            magGSMin = messages.getNotNothingString("magGSMin", errorInMethod);
            magGSMinTooltip = messages.getNotNothingString("magGSMinTooltip", errorInMethod);
            magGSMax = messages.getNotNothingString("magGSMax", errorInMethod);
            magGSMaxTooltip = messages.getNotNothingString("magGSMaxTooltip", errorInMethod);
            magGSNSections = messages.getNotNothingString("magGSNSections", errorInMethod);
            magGSNSectionsTooltip = messages.getNotNothingString("magGSNSectionsTooltip", errorInMethod);
            magGSLandMask = messages.getNotNothingString("magGSLandMask", errorInMethod);
            magGSLandMaskTooltipGrid = messages.getNotNothingString("magGSLandMaskTooltipGrid", errorInMethod);
            magGSLandMaskTooltipTable = messages.getNotNothingString("magGSLandMaskTooltipTable", errorInMethod);
            magGSVectorStandard = messages.getNotNothingString("magGSVectorStandard", errorInMethod);
            magGSVectorStandardTooltip = messages.getNotNothingString("magGSVectorStandardTooltip", errorInMethod);
            magGSYAscendingTooltip = messages.getNotNothingString("magGSYAscendingTooltip", errorInMethod);
            magGSYAxisMin = messages.getNotNothingString("magGSYAxisMin", errorInMethod);
            magGSYAxisMax = messages.getNotNothingString("magGSYAxisMax", errorInMethod);
            magGSYRangeMinTooltip = messages.getNotNothingString("magGSYRangeMinTooltip", errorInMethod);
            magGSYRangeMaxTooltip = messages.getNotNothingString("magGSYRangeMaxTooltip", errorInMethod);
            magGSYRangeTooltip = messages.getNotNothingString("magGSYRangeTooltip", errorInMethod);
            magItemFirst = messages.getNotNothingString("magItemFirst", errorInMethod);
            magItemPrevious = messages.getNotNothingString("magItemPrevious", errorInMethod);
            magItemNext = messages.getNotNothingString("magItemNext", errorInMethod);
            magItemLast = messages.getNotNothingString("magItemLast", errorInMethod);
            magJust1Value = messages.getNotNothingString("magJust1Value", errorInMethod);
            magRange = messages.getNotNothingString("magRange", errorInMethod);
            magRangeTo = messages.getNotNothingString("magRangeTo", errorInMethod);
            magRedraw = messages.getNotNothingString("magRedraw", errorInMethod);
            magRedrawTooltip = messages.getNotNothingString("magRedrawTooltip", errorInMethod);
            magTimeRange = messages.getNotNothingString("magTimeRange", errorInMethod);
            magTimeRangeFirst = messages.getNotNothingString("magTimeRangeFirst", errorInMethod);
            magTimeRangeBack = messages.getNotNothingString("magTimeRangeBack", errorInMethod);
            magTimeRangeForward = messages.getNotNothingString("magTimeRangeForward", errorInMethod);
            magTimeRangeLast = messages.getNotNothingString("magTimeRangeLast", errorInMethod);
            magTimeRangeTooltip = messages.getNotNothingString("magTimeRangeTooltip", errorInMethod);
            magTimeRangeTooltip2 = messages.getNotNothingString("magTimeRangeTooltip2", errorInMethod);
            magTimesVary = messages.getNotNothingString("magTimesVary", errorInMethod);
            magViewUrl = messages.getNotNothingString("magViewUrl", errorInMethod);
            magZoom = messages.getNotNothingString("magZoom", errorInMethod);
            magZoomCenter = messages.getNotNothingString("magZoomCenter", errorInMethod);
            magZoomCenterTooltip = messages.getNotNothingString("magZoomCenterTooltip", errorInMethod);
            magZoomIn = messages.getNotNothingString("magZoomIn", errorInMethod);
            magZoomInTooltip = messages.getNotNothingString("magZoomInTooltip", errorInMethod);
            magZoomOut = messages.getNotNothingString("magZoomOut", errorInMethod);
            magZoomOutTooltip = messages.getNotNothingString("magZoomOutTooltip", errorInMethod);
            magZoomALittle = messages.getNotNothingString("magZoomALittle", errorInMethod);
            magZoomData = messages.getNotNothingString("magZoomData", errorInMethod);
            magZoomOutData = messages.getNotNothingString("magZoomOutData", errorInMethod);
            magGridTooltip = messages.getNotNothingString("magGridTooltip", errorInMethod);
            magTableTooltip = messages.getNotNothingString("magTableTooltip", errorInMethod);
            Math2.memoryTooMuchData = messages.getNotNothingString("memoryTooMuchData", errorInMethod);
            Math2.memoryArraySize = messages.getNotNothingString("memoryArraySize", errorInMethod);
            Math2.memoryThanCurrentlySafe = messages.getNotNothingString("memoryThanCurrentlySafe", errorInMethod);
            Math2.memoryThanSafe = messages.getNotNothingString("memoryThanSafe", errorInMethod);
            metadataDownload = messages.getNotNothingString("metadataDownload", errorInMethod);
            moreInformation = messages.getNotNothingString("moreInformation", errorInMethod);
            MustBe.THERE_IS_NO_DATA = messages.getNotNothingString("MustBeThereIsNoData", errorInMethod);
            MustBe.NotNull = messages.getNotNothingString("MustBeNotNull", errorInMethod);
            MustBe.NotEmpty = messages.getNotNothingString("MustBeNotEmpty", errorInMethod);
            MustBe.InternalError = messages.getNotNothingString("MustBeInternalError", errorInMethod);
            MustBe.OutOfMemoryError = messages.getNotNothingString("MustBeOutOfMemoryError", errorInMethod);
            nMatching1 = messages.getNotNothingString("nMatching1", errorInMethod);
            nMatching = messages.getNotNothingString("nMatching", errorInMethod);
            nMatchingAlphabetical = messages.getNotNothingString("nMatchingAlphabetical", errorInMethod);
            nMatchingMostRelevant = messages.getNotNothingString("nMatchingMostRelevant", errorInMethod);
            nMatchingPage = messages.getNotNothingString("nMatchingPage", errorInMethod);
            nMatchingCurrent = messages.getNotNothingString("nMatchingCurrent", errorInMethod);
            noDataFixedValue = messages.getNotNothingString("noDataFixedValue", errorInMethod);
            noDataNoLL = messages.getNotNothingString("noDataNoLL", errorInMethod);
            noDatasetWith = messages.getNotNothingString("noDatasetWith", errorInMethod);
            noPage1 = messages.getNotNothingString("noPage1", errorInMethod);
            noPage2 = messages.getNotNothingString("noPage2", errorInMethod);
            notAllowed = messages.getNotNothingString("notAllowed", errorInMethod);
            notAuthorized = messages.getNotNothingString("notAuthorized", errorInMethod);
            notAuthorizedForData = messages.getNotNothingString("notAuthorizedForData", errorInMethod);
            notAvailable = messages.getNotNothingString("notAvailable", errorInMethod);
            noXxx = messages.getNotNothingString("noXxx", errorInMethod);
            noXxxBecause = messages.getNotNothingString("noXxxBecause", errorInMethod);
            noXxxBecause2 = messages.getNotNothingString("noXxxBecause2", errorInMethod);
            noXxxNotActive = messages.getNotNothingString("noXxxNotActive", errorInMethod);
            noXxxNoAxis1 = messages.getNotNothingString("noXxxNoAxis1", errorInMethod);
            noXxxNoCdmDataType = messages.getNotNothingString("noXxxNoCdmDataType", errorInMethod);
            noXxxNoColorBar = messages.getNotNothingString("noXxxNoColorBar", errorInMethod);
            noXxxNoLL = messages.getNotNothingString("noXxxNoLL", errorInMethod);
            noXxxNoLLEvenlySpaced = messages.getNotNothingString("noXxxNoLLEvenlySpaced", errorInMethod);
            noXxxNoLLGt1 = messages.getNotNothingString("noXxxNoLLGt1", errorInMethod);
            noXxxNoLLT = messages.getNotNothingString("noXxxNoLLT", errorInMethod);
            noXxxNoLonIn180 = messages.getNotNothingString("noXxxNoLonIn180", errorInMethod);
            noXxxNoNonString = messages.getNotNothingString("noXxxNoNonString", errorInMethod);
            noXxxNo2NonString = messages.getNotNothingString("noXxxNo2NonString", errorInMethod);
            noXxxNoStation = messages.getNotNothingString("noXxxNoStation", errorInMethod);
            noXxxNoStationID = messages.getNotNothingString("noXxxNoStationID", errorInMethod);
            noXxxNoSubsetVariables = messages.getNotNothingString("noXxxNoSubsetVariables", errorInMethod);
            noXxxNoOLLSubsetVariables = messages.getNotNothingString("noXxxNoOLLSubsetVariables", errorInMethod);
            noXxxNoMinMax = messages.getNotNothingString("noXxxNoMinMax", errorInMethod);
            noXxxItsGridded = messages.getNotNothingString("noXxxItsGridded", errorInMethod);
            noXxxItsTabular = messages.getNotNothingString("noXxxItsTabular", errorInMethod);
            optional = messages.getNotNothingString("optional", errorInMethod);
            options = messages.getNotNothingString("options", errorInMethod);
            orRefineSearchWith = messages.getNotNothingString("orRefineSearchWith", errorInMethod);
            orRefineSearchWith += " ";
            orSearchWith = messages.getNotNothingString("orSearchWith", errorInMethod);
            orSearchWith += " ";
            orComma = messages.getNotNothingString("orComma", errorInMethod);
            orComma += " ";
            outOfDateHtml = messages.getNotNothingString("outOfDateHtml", errorInMethod);
            palettes = String2.split(messages.getNotNothingString("palettes", errorInMethod), ',');
            palettes0 = new String[palettes.length + 1];
            palettes0[0] = "";
            System.arraycopy(palettes, 0, palettes0, 1, palettes.length);
            patientData = messages.getNotNothingString("patientData", errorInMethod);
            patientYourGraph = messages.getNotNothingString("patientYourGraph", errorInMethod);
            pdfWidths = String2
                    .toIntArray(String2.split(messages.getNotNothingString("pdfWidths", errorInMethod), ','));
            pdfHeights = String2
                    .toIntArray(String2.split(messages.getNotNothingString("pdfHeights", errorInMethod), ','));
            percentEncode = messages.getNotNothingString("percentEncode", errorInMethod);
            pickADataset = messages.getNotNothingString("pickADataset", errorInMethod);
            protocolSearchHtml = messages.getNotNothingString("protocolSearchHtml", errorInMethod);
            protocolSearch2Html = messages.getNotNothingString("protocolSearch2Html", errorInMethod);
            protocolClick = messages.getNotNothingString("protocolClick", errorInMethod);
            queryError = messages.getNotNothingString("queryError", errorInMethod) + " ";
            Table.QUERY_ERROR = queryError;
            queryError180 = messages.getNotNothingString("queryError180", errorInMethod);
            queryError1Value = messages.getNotNothingString("queryError1Value", errorInMethod);
            queryError1Var = messages.getNotNothingString("queryError1Var", errorInMethod);
            queryError2Var = messages.getNotNothingString("queryError2Var", errorInMethod);
            queryErrorActualRange = messages.getNotNothingString("queryErrorActualRange", errorInMethod);
            queryErrorAdjusted = messages.getNotNothingString("queryErrorAdjusted", errorInMethod);
            queryErrorAscending = messages.getNotNothingString("queryErrorAscending", errorInMethod);
            queryErrorConstraintNaN = messages.getNotNothingString("queryErrorConstraintNaN", errorInMethod);
            queryErrorEqualSpacing = messages.getNotNothingString("queryErrorEqualSpacing", errorInMethod);
            queryErrorExpectedAt = messages.getNotNothingString("queryErrorExpectedAt", errorInMethod);
            queryErrorFileType = messages.getNotNothingString("queryErrorFileType", errorInMethod);
            queryErrorInvalid = messages.getNotNothingString("queryErrorInvalid", errorInMethod);
            queryErrorLL = messages.getNotNothingString("queryErrorLL", errorInMethod);
            queryErrorLLGt1 = messages.getNotNothingString("queryErrorLLGt1", errorInMethod);
            queryErrorLLT = messages.getNotNothingString("queryErrorLLT", errorInMethod);
            queryErrorNeverTrue = messages.getNotNothingString("queryErrorNeverTrue", errorInMethod);
            queryErrorNeverBothTrue = messages.getNotNothingString("queryErrorNeverBothTrue", errorInMethod);
            queryErrorNotAxis = messages.getNotNothingString("queryErrorNotAxis", errorInMethod);
            queryErrorNotExpectedAt = messages.getNotNothingString("queryErrorNotExpectedAt", errorInMethod);
            queryErrorNotFoundAfter = messages.getNotNothingString("queryErrorNotFoundAfter", errorInMethod);
            queryErrorOccursTwice = messages.getNotNothingString("queryErrorOccursTwice", errorInMethod);
            Table.ORDER_BY_CLOSEST_ERROR = messages.getNotNothingString("queryErrorOrderByClosest", errorInMethod);
            Table.ORDER_BY_LIMIT_ERROR = messages.getNotNothingString("queryErrorOrderByLimit", errorInMethod);
            queryErrorOrderByVariable = messages.getNotNothingString("queryErrorOrderByVariable", errorInMethod);
            queryErrorUnknownVariable = messages.getNotNothingString("queryErrorUnknownVariable", errorInMethod);

            queryErrorGrid1Axis = messages.getNotNothingString("queryErrorGrid1Axis", errorInMethod);
            queryErrorGridAmp = messages.getNotNothingString("queryErrorGridAmp", errorInMethod);
            queryErrorGridDiagnostic = messages.getNotNothingString("queryErrorGridDiagnostic", errorInMethod);
            queryErrorGridBetween = messages.getNotNothingString("queryErrorGridBetween", errorInMethod);
            queryErrorGridLessMin = messages.getNotNothingString("queryErrorGridLessMin", errorInMethod);
            queryErrorGridGreaterMax = messages.getNotNothingString("queryErrorGridGreaterMax", errorInMethod);
            queryErrorGridMissing = messages.getNotNothingString("queryErrorGridMissing", errorInMethod);
            queryErrorGridNoAxisVar = messages.getNotNothingString("queryErrorGridNoAxisVar", errorInMethod);
            queryErrorGridNoDataVar = messages.getNotNothingString("queryErrorGridNoDataVar", errorInMethod);
            queryErrorGridNotIdentical = messages.getNotNothingString("queryErrorGridNotIdentical", errorInMethod);
            queryErrorGridSLessS = messages.getNotNothingString("queryErrorGridSLessS", errorInMethod);
            queryErrorLastEndP = messages.getNotNothingString("queryErrorLastEndP", errorInMethod);
            queryErrorLastExpected = messages.getNotNothingString("queryErrorLastExpected", errorInMethod);
            queryErrorLastUnexpected = messages.getNotNothingString("queryErrorLastUnexpected", errorInMethod);
            queryErrorLastPMInvalid = messages.getNotNothingString("queryErrorLastPMInvalid", errorInMethod);
            queryErrorLastPMInteger = messages.getNotNothingString("queryErrorLastPMInteger", errorInMethod);
            rangesFromTo = messages.getNotNothingString("rangesFromTo", errorInMethod);
            requestFormatExamplesHtml = messages.getNotNothingString("requestFormatExamplesHtml", errorInMethod);
            resetTheForm = messages.getNotNothingString("resetTheForm", errorInMethod);
            resetTheFormWas = messages.getNotNothingString("resetTheFormWas", errorInMethod);
            resourceNotFound = messages.getNotNothingString("resourceNotFound", errorInMethod);
            resultsFormatExamplesHtml = messages.getNotNothingString("resultsFormatExamplesHtml", errorInMethod);
            resultsOfSearchFor = messages.getNotNothingString("resultsOfSearchFor", errorInMethod);
            restfulInformationFormats = messages.getNotNothingString("restfulInformationFormats", errorInMethod);
            restfulViaService = messages.getNotNothingString("restfulViaService", errorInMethod);
            rows = messages.getNotNothingString("rows", errorInMethod);
            rssNo = messages.getNotNothingString("rssNo", errorInMethod);
            searchTitle = messages.getNotNothingString("searchTitle", errorInMethod);
            searchDoFullTextHtml = messages.getNotNothingString("searchDoFullTextHtml", errorInMethod);
            searchFullTextHtml = messages.getNotNothingString("searchFullTextHtml", errorInMethod);
            searchButton = messages.getNotNothingString("searchButton", errorInMethod);
            searchClickTip = messages.getNotNothingString("searchClickTip", errorInMethod);
            searchHintsLuceneTooltip = messages.getNotNothingString("searchHintsLuceneTooltip", errorInMethod);
            searchHintsOriginalTooltip = messages.getNotNothingString("searchHintsOriginalTooltip", errorInMethod);
            searchHintsTooltip = messages.getNotNothingString("searchHintsTooltip", errorInMethod);
            searchNotAvailable = messages.getNotNothingString("searchNotAvailable", errorInMethod);
            searchTip = messages.getNotNothingString("searchTip", errorInMethod);
            searchSpelling = messages.getNotNothingString("searchSpelling", errorInMethod);
            searchFewerWords = messages.getNotNothingString("searchFewerWords", errorInMethod);
            searchWithQuery = messages.getNotNothingString("searchWithQuery", errorInMethod);
            selectNext = messages.getNotNothingString("selectNext", errorInMethod);
            selectPrevious = messages.getNotNothingString("selectPrevious", errorInMethod);
            seeProtocolDocumentation = messages.getNotNothingString("seeProtocolDocumentation", errorInMethod);
            sosDescriptionHtml = messages.getNotNothingString("sosDescriptionHtml", errorInMethod);
            sosLongDescriptionHtml = messages.getNotNothingString("sosLongDescriptionHtml", errorInMethod);
            sparqlP01toP02pre = messages.getNotNothingString("sparqlP01toP02pre", errorInMethod);
            sparqlP01toP02post = messages.getNotNothingString("sparqlP01toP02post", errorInMethod);
            ssUse = messages.getNotNothingString("ssUse", errorInMethod);
            ssUsePlain = XML.removeHTMLTags(ssUse);
            ssBePatient = messages.getNotNothingString("ssBePatient", errorInMethod);
            ssInstructionsHtml = messages.getNotNothingString("ssInstructionsHtml", errorInMethod);
            standardShortDescriptionHtml = messages.getNotNothingString("standardShortDescriptionHtml",
                    errorInMethod);
            standardShortDescriptionHtml = setup.getString("standardShortDescriptionHtml",
                    standardShortDescriptionHtml);
            standardLicense = messages.getNotNothingString("standardLicense", errorInMethod);
            standardLicense = setup.getString("standardLicense", standardLicense);
            standardContact = messages.getNotNothingString("standardContact", errorInMethod);
            standardContact = setup.getString("standardContact", standardContact);
            standardDataLicenses = messages.getNotNothingString("standardDataLicenses", errorInMethod);
            standardDataLicenses = setup.getString("standardDataLicenses", standardDataLicenses);
            standardDisclaimerOfExternalLinks = messages.getNotNothingString("standardDisclaimerOfExternalLinks",
                    errorInMethod);
            standardDisclaimerOfExternalLinks = setup.getString("standardDisclaimerOfExternalLinks",
                    standardDisclaimerOfExternalLinks);
            standardDisclaimerOfEndorsement = messages.getNotNothingString("standardDisclaimerOfEndorsement",
                    errorInMethod);
            standardDisclaimerOfEndorsement = setup.getString("standardDisclaimerOfEndorsement",
                    standardDisclaimerOfEndorsement);
            standardGeneralDisclaimer = messages.getNotNothingString("standardGeneralDisclaimer", errorInMethod);
            standardGeneralDisclaimer = setup.getString("standardGeneralDisclaimer", standardGeneralDisclaimer);
            standardPrivacyPolicy = messages.getNotNothingString("standardPrivacyPolicy", errorInMethod);
            standardPrivacyPolicy = setup.getString("standardPrivacyPolicy", standardPrivacyPolicy);

            startHeadHtml = messages.getNotNothingString("startHeadHtml5", errorInMethod);
            startHeadHtml = setup.getString("startHeadHtml5", startHeadHtml);
            startBodyHtml = messages.getNotNothingString("startBodyHtml5", errorInMethod);
            startBodyHtml = setup.getString("startBodyHtml5", startBodyHtml);
            endBodyHtml = messages.getNotNothingString("endBodyHtml5", errorInMethod);
            endBodyHtml = setup.getString("endBodyHtml5", endBodyHtml);
            endBodyHtml = String2.replaceAll(endBodyHtml, "&erddapVersion;", erddapVersion);
            //ensure HTML5
            Test.ensureTrue(startHeadHtml.startsWith("<!DOCTYPE html>"),
                    "<startHeadHtml> in setup.xml must start with \"<!DOCTYPE html>\".");

            statusHtml = messages.getNotNothingString("statusHtml", errorInMethod);
            submit = messages.getNotNothingString("submit", errorInMethod);
            submitTooltip = messages.getNotNothingString("submitTooltip", errorInMethod);
            subscriptionsTitle = messages.getNotNothingString("subscriptionsTitle", errorInMethod);
            subscriptionAdd = messages.getNotNothingString("subscriptionAdd", errorInMethod);
            subscriptionValidate = messages.getNotNothingString("subscriptionValidate", errorInMethod);
            subscriptionList = messages.getNotNothingString("subscriptionList", errorInMethod);
            subscriptionRemove = messages.getNotNothingString("subscriptionRemove", errorInMethod);
            subscription0Html = messages.getNotNothingString("subscription0Html", errorInMethod);
            subscription1Html = messages.getNotNothingString("subscription1Html", errorInMethod);
            subscription2Html = messages.getNotNothingString("subscription2Html", errorInMethod);
            subscriptionAbuse = messages.getNotNothingString("subscriptionAbuse", errorInMethod);
            subscriptionAddError = messages.getNotNothingString("subscriptionAddError", errorInMethod);
            subscriptionAddHtml = messages.getNotNothingString("subscriptionAddHtml", errorInMethod);
            subscriptionAdd2 = messages.getNotNothingString("subscriptionAdd2", errorInMethod);
            subscriptionAddSuccess = messages.getNotNothingString("subscriptionAddSuccess", errorInMethod);
            subscriptionEmail = messages.getNotNothingString("subscriptionEmail", errorInMethod);
            subscriptionEmailInvalid = messages.getNotNothingString("subscriptionEmailInvalid", errorInMethod);
            subscriptionEmailTooLong = messages.getNotNothingString("subscriptionEmailTooLong", errorInMethod);
            subscriptionEmailUnspecified = messages.getNotNothingString("subscriptionEmailUnspecified",
                    errorInMethod);
            subscriptionIDInvalid = messages.getNotNothingString("subscriptionIDInvalid", errorInMethod);
            subscriptionIDTooLong = messages.getNotNothingString("subscriptionIDTooLong", errorInMethod);
            subscriptionIDUnspecified = messages.getNotNothingString("subscriptionIDUnspecified", errorInMethod);
            subscriptionKeyInvalid = messages.getNotNothingString("subscriptionKeyInvalid", errorInMethod);
            subscriptionKeyUnspecified = messages.getNotNothingString("subscriptionKeyUnspecified", errorInMethod);
            subscriptionListError = messages.getNotNothingString("subscriptionListError", errorInMethod);
            subscriptionListHtml = messages.getNotNothingString("subscriptionListHtml", errorInMethod);
            subscriptionListSuccess = messages.getNotNothingString("subscriptionListSuccess", errorInMethod);
            subscriptionRemoveError = messages.getNotNothingString("subscriptionRemoveError", errorInMethod);
            subscriptionRemoveHtml = messages.getNotNothingString("subscriptionRemoveHtml", errorInMethod);
            subscriptionRemove2 = messages.getNotNothingString("subscriptionRemove2", errorInMethod);
            subscriptionRemoveSuccess = messages.getNotNothingString("subscriptionRemoveSuccess", errorInMethod);
            subscriptionRSS = messages.getNotNothingString("subscriptionRSS", errorInMethod);
            subscriptionsNotAvailable = messages.getNotNothingString("subscriptionsNotAvailable", errorInMethod);
            subscriptionUrlHtml = messages.getNotNothingString("subscriptionUrlHtml", errorInMethod);
            subscriptionUrlInvalid = messages.getNotNothingString("subscriptionUrlInvalid", errorInMethod);
            subscriptionUrlTooLong = messages.getNotNothingString("subscriptionUrlTooLong", errorInMethod);
            subscriptionValidateError = messages.getNotNothingString("subscriptionValidateError", errorInMethod);
            subscriptionValidateHtml = messages.getNotNothingString("subscriptionValidateHtml", errorInMethod);
            subscriptionValidateSuccess = messages.getNotNothingString("subscriptionValidateSuccess",
                    errorInMethod);
            subset = messages.getNotNothingString("subset", errorInMethod);
            subsetSelect = messages.getNotNothingString("subsetSelect", errorInMethod);
            subsetNMatching = messages.getNotNothingString("subsetNMatching", errorInMethod);
            subsetInstructions = messages.getNotNothingString("subsetInstructions", errorInMethod);
            subsetOption = messages.getNotNothingString("subsetOption", errorInMethod);
            subsetOptions = messages.getNotNothingString("subsetOptions", errorInMethod);
            subsetRefineMapDownload = messages.getNotNothingString("subsetRefineMapDownload", errorInMethod);
            subsetRefineSubsetDownload = messages.getNotNothingString("subsetRefineSubsetDownload", errorInMethod);
            subsetClickResetClosest = messages.getNotNothingString("subsetClickResetClosest", errorInMethod);
            subsetClickResetLL = messages.getNotNothingString("subsetClickResetLL", errorInMethod);
            subsetMetadata = messages.getNotNothingString("subsetMetadata", errorInMethod);
            subsetCount = messages.getNotNothingString("subsetCount", errorInMethod);
            subsetPercent = messages.getNotNothingString("subsetPercent", errorInMethod);
            subsetViewSelect = messages.getNotNothingString("subsetViewSelect", errorInMethod);
            subsetViewSelectDistinctCombos = messages.getNotNothingString("subsetViewSelectDistinctCombos",
                    errorInMethod);
            subsetViewSelectRelatedCounts = messages.getNotNothingString("subsetViewSelectRelatedCounts",
                    errorInMethod);
            subsetWhen = messages.getNotNothingString("subsetWhen", errorInMethod);
            subsetWhenNoConstraints = messages.getNotNothingString("subsetWhenNoConstraints", errorInMethod);
            subsetWhenCounts = messages.getNotNothingString("subsetWhenCounts", errorInMethod);
            subsetComboClickSelect = messages.getNotNothingString("subsetComboClickSelect", errorInMethod);
            subsetNVariableCombos = messages.getNotNothingString("subsetNVariableCombos", errorInMethod);
            subsetShowingAllRows = messages.getNotNothingString("subsetShowingAllRows", errorInMethod);
            subsetShowingNRows = messages.getNotNothingString("subsetShowingNRows", errorInMethod);
            subsetChangeShowing = messages.getNotNothingString("subsetChangeShowing", errorInMethod);
            subsetNRowsRelatedData = messages.getNotNothingString("subsetNRowsRelatedData", errorInMethod);
            subsetViewRelatedChange = messages.getNotNothingString("subsetViewRelatedChange", errorInMethod);
            subsetTotalCount = messages.getNotNothingString("subsetTotalCount", errorInMethod);
            subsetView = messages.getNotNothingString("subsetView", errorInMethod);
            subsetViewCheck = messages.getNotNothingString("subsetViewCheck", errorInMethod);
            subsetViewCheck1 = messages.getNotNothingString("subsetViewCheck1", errorInMethod);
            subsetViewDistinctMap = messages.getNotNothingString("subsetViewDistinctMap", errorInMethod);
            subsetViewRelatedMap = messages.getNotNothingString("subsetViewRelatedMap", errorInMethod);
            subsetViewDistinctDataCounts = messages.getNotNothingString("subsetViewDistinctDataCounts",
                    errorInMethod);
            subsetViewDistinctData = messages.getNotNothingString("subsetViewDistinctData", errorInMethod);
            subsetViewRelatedDataCounts = messages.getNotNothingString("subsetViewRelatedDataCounts",
                    errorInMethod);
            subsetViewRelatedData = messages.getNotNothingString("subsetViewRelatedData", errorInMethod);
            subsetViewDistinctMapTooltip = messages.getNotNothingString("subsetViewDistinctMapTooltip",
                    errorInMethod);
            subsetViewRelatedMapTooltip = messages.getNotNothingString("subsetViewRelatedMapTooltip",
                    errorInMethod);
            subsetViewDistinctDataCountsTooltip = messages
                    .getNotNothingString("subsetViewDistinctDataCountsTooltip", errorInMethod);
            subsetViewDistinctDataTooltip = messages.getNotNothingString("subsetViewDistinctDataTooltip",
                    errorInMethod);
            subsetViewRelatedDataCountsTooltip = messages.getNotNothingString("subsetViewRelatedDataCountsTooltip",
                    errorInMethod);
            subsetViewRelatedDataTooltip = messages.getNotNothingString("subsetViewRelatedDataTooltip",
                    errorInMethod);
            subsetWarn = messages.getNotNothingString("subsetWarn", errorInMethod);
            subsetWarn10000 = messages.getNotNothingString("subsetWarn10000", errorInMethod);
            subsetTooltip = messages.getNotNothingString("subsetTooltip", errorInMethod);
            subsetNotSetUp = messages.getNotNothingString("subsetNotSetUp", errorInMethod);
            subsetLongNotShown = messages.getNotNothingString("subsetLongNotShown", errorInMethod);

            tabledapVideoIntro = messages.getNotNothingString("tabledapVideoIntro", errorInMethod);
            theLongDescriptionHtml = messages.getNotNothingString("theLongDescriptionHtml", errorInMethod);
            Then = messages.getNotNothingString("Then", errorInMethod);
            unknownDatasetID = messages.getNotNothingString("unknownDatasetID", errorInMethod);
            unknownProtocol = messages.getNotNothingString("unknownProtocol", errorInMethod);
            unsupportedFileType = messages.getNotNothingString("unsupportedFileType", errorInMethod);
            String tUpdateUrls[] = String2.split(messages.getNotNothingString("updateUrls", errorInMethod) + "\n",
                    '\n'); // +\n since xml content is trimmed.
            updateUrlsSkipAttributes = StringArray
                    .arrayFromCSV(messages.getNotNothingString("updateUrlsSkipAttributes", errorInMethod));
            viewAllDatasetsHtml = messages.getNotNothingString("viewAllDatasetsHtml", errorInMethod);
            waitThenTryAgain = messages.getNotNothingString("waitThenTryAgain", errorInMethod);
            gov.noaa.pfel.erddap.dataset.WaitThenTryAgainException.waitThenTryAgain = waitThenTryAgain;
            warning = messages.getNotNothingString("warning", errorInMethod);
            wcsDescriptionHtml = messages.getNotNothingString("wcsDescriptionHtml", errorInMethod);
            wcsLongDescriptionHtml = messages.getNotNothingString("wcsLongDescriptionHtml", errorInMethod);
            wmsDescriptionHtml = messages.getNotNothingString("wmsDescriptionHtml", errorInMethod);
            wmsInstructions = messages.getNotNothingString("wmsInstructions", errorInMethod);
            wmsLongDescriptionHtml = messages.getNotNothingString("wmsLongDescriptionHtml", errorInMethod);
            wmsManyDatasets = messages.getNotNothingString("wmsManyDatasets", errorInMethod);

            Test.ensureEqual(imageWidths.length, 3, "imageWidths.length must be 3.");
            Test.ensureEqual(imageHeights.length, 3, "imageHeights.length must be 3.");
            Test.ensureEqual(pdfWidths.length, 3, "pdfWidths.length must be 3.");
            Test.ensureEqual(pdfHeights.length, 3, "pdfHeights.length must be 3.");

            Test.ensureEqual(tUpdateUrls.length % 3, 0,
                    "updateUrls must have 1 or more groups of 3 lines: from, to, blank.");
            int nUpdateUrls = tUpdateUrls.length / 3;
            updateUrlsFrom = new String[nUpdateUrls];
            updateUrlsTo = new String[nUpdateUrls];
            for (int i = 0; i < nUpdateUrls; i++) {
                int i3 = i * 3;
                updateUrlsFrom[i] = tUpdateUrls[i3];
                updateUrlsTo[i] = tUpdateUrls[i3 + 1];
                Test.ensureTrue(String2.isSomething(tUpdateUrls[i3]),
                        "updateUrls line #" + (i3 + 0) + " is empty.");
                Test.ensureTrue(String2.isSomething(tUpdateUrls[i3 + 1]),
                        "updateUrls line #" + (i3 + 0) + " is empty.");
                Test.ensureEqual(tUpdateUrls[i3 + 2], "", "updateUrls line #" + (i3 + 0) + " isn't empty.");
            }

            for (int p = 0; p < palettes.length; p++) {
                String tName = fullPaletteDirectory + palettes[p] + ".cpt";
                Test.ensureTrue(File2.isFile(tName),
                        "\"" + palettes[p] + "\" is listed in <palettes>, but there is no file " + tName);
            }

            //try to create an nc4 file
            accessibleViaNC4 = ".nc4 is not yet supported.";
            /* DISABLED until nc4 is thread safe -- next netcdf-java
                    String testNc4Name = fullTestCacheDirectory + 
                        "testNC4_" + Calendar2.getCompactCurrentISODateTimeStringLocal() + ".nc";
                    //String2.log("testNc4Name=" + testNc4Name);
                    try {
                        NetcdfFileWriter nc = NetcdfFileWriter.createNew(
            NetcdfFileWriter.Version.netcdf4, testNc4Name);
                        try {
            Group rootGroup = nc.addGroup(null, "");
            nc.setFill(false);
                            
            int nRows = 4;
            Dimension dimension  = nc.addDimension(rootGroup, "row", nRows);
            Variable var = nc.addVariable(rootGroup, "myLongs", 
                DataType.getType(long.class), Arrays.asList(dimension)); 
                
            //leave "define" mode
            nc.create();  //error is thrown here if netcdf-c not found
                
            //write the data
            Array array = Array.factory(long.class, new int[]{nRows}, new long[]{0,1,2,3});
            nc.write(var, new int[]{0}, array);
                
            //if close throws Throwable, it is trouble
            nc.close(); //it calls flush() and doesn't like flush called separately
                
            //success!
            accessibleViaNC4 = "";
            String2.log(".nc4 files can be created in this ERDDAP installation.");
                
                        } catch (Throwable t2) {
            //try to close the file
            try {
                nc.close(); //it calls flush() and doesn't like flush called separately
            } catch (Throwable t3) {
                //don't care
            }
                
            throw t2;
                        }
                
                    } catch (Throwable t) {
                        accessibleViaNC4 = String2.canonical(
            MessageFormat.format(noXxxBecause2, ".nc4",
                resourceNotFound + " netcdf-c library"));
                        String2.log(t.toString() + "\n" + accessibleViaNC4);
                    }
            //        File2.delete(testNc4Name);
            */

            ampLoginInfoPo = startBodyHtml.indexOf(ampLoginInfo);
            //String2.log("ampLoginInfoPo=" + ampLoginInfoPo);

            searchHintsTooltip = "<div class=\"standard_max_width\">" + searchHintsTooltip + "\n"
                    + (useLuceneSearchEngine ? searchHintsLuceneTooltip : searchHintsOriginalTooltip) + "</div>";
            advancedSearchDirections = String2.replaceAll(advancedSearchDirections, "&searchButton;", searchButton);

            //always non-https: url
            convertOceanicAtmosphericAcronymsService = MessageFormat
                    .format(convertOceanicAtmosphericAcronymsService, erddapUrl) + "\n";
            convertOceanicAtmosphericVariableNamesService = MessageFormat
                    .format(convertOceanicAtmosphericVariableNamesService, erddapUrl) + "\n";
            convertFipsCountyService = MessageFormat.format(convertFipsCountyService, erddapUrl) + "\n";
            convertKeywordsService = MessageFormat.format(convertKeywordsService, erddapUrl) + "\n";
            convertTimeNotes = MessageFormat.format(convertTimeNotes, erddapUrl, convertTimeUnitsHelp) + "\n";
            convertTimeService = MessageFormat.format(convertTimeService, erddapUrl) + "\n";
            convertUnitsFilter = MessageFormat.format(convertUnitsFilter, erddapUrl, units_standard) + "\n";
            convertUnitsService = MessageFormat.format(convertUnitsService, erddapUrl) + "\n";

            //standardContact is used by legal
            String tEmail = SSR.getSafeEmailAddress(adminEmail);
            filesDocumentation = String2.replaceAll(filesDocumentation, "&adminEmail;", tEmail);
            standardContact = String2.replaceAll(standardContact, "&adminEmail;", tEmail);
            legal = String2.replaceAll(legal, "[standardContact]", standardContact + "\n\n");
            legal = String2.replaceAll(legal, "[standardDataLicenses]", standardDataLicenses + "\n\n");
            legal = String2.replaceAll(legal, "[standardDisclaimerOfExternalLinks]",
                    standardDisclaimerOfExternalLinks + "\n\n");
            legal = String2.replaceAll(legal, "[standardDisclaimerOfEndorsement]",
                    standardDisclaimerOfEndorsement + "\n\n");
            legal = String2.replaceAll(legal, "[standardPrivacyPolicy]", standardPrivacyPolicy + "\n\n");

            loginProblems = String2.replaceAll(loginProblems, "&cookiesHelp;", cookiesHelp);
            loginProblems = String2.replaceAll(loginProblems, "&adminContact;", adminContact()) + "\n\n";
            loginProblemsAfter = String2.replaceAll(loginProblemsAfter, "&adminContact;", adminContact()) + "\n\n";
            loginPublicAccess += "\n";
            logoutSuccess += "\n";

            doWithGraphs = String2.replaceAll(doWithGraphs, "&ssUse;", slideSorterActive ? ssUse : "");

            theLongDescriptionHtml = String2.replaceAll(theLongDescriptionHtml, "&ssUse;",
                    slideSorterActive ? ssUse : "");
            theLongDescriptionHtml = String2.replaceAll(theLongDescriptionHtml, "&requestFormatExamplesHtml;",
                    requestFormatExamplesHtml);
            theLongDescriptionHtml = String2.replaceAll(theLongDescriptionHtml, "&resultsFormatExamplesHtml;",
                    resultsFormatExamplesHtml);

            standardShortDescriptionHtml = String2.replaceAll(standardShortDescriptionHtml,
                    "&convertTimeReference;", convertersActive ? convertTimeReference : "");
            standardShortDescriptionHtml = String2.replaceAll(standardShortDescriptionHtml, "&wmsManyDatasets;",
                    wmsActive ? wmsManyDatasets : "");

            theShortDescriptionHtml = String2.replaceAll(theShortDescriptionHtml, "[standardShortDescriptionHtml]",
                    standardShortDescriptionHtml);
            theShortDescriptionHtml = String2.replaceAll(theShortDescriptionHtml, "&requestFormatExamplesHtml;",
                    requestFormatExamplesHtml);
            theShortDescriptionHtml = String2.replaceAll(theShortDescriptionHtml, "&resultsFormatExamplesHtml;",
                    resultsFormatExamplesHtml);

            try {
                computerName = System.getenv("COMPUTERNAME"); //windows 
                if (computerName == null)
                    computerName = System.getenv("HOSTNAME"); //linux
                if (computerName == null)
                    computerName = java.net.InetAddress.getLocalHost().getHostName(); //coastwatch.pfeg.noaa.gov
                if (computerName == null)
                    computerName = "";
                int dotPo = computerName.indexOf('.');
                if (dotPo > 0)
                    computerName = computerName.substring(0, dotPo);
            } catch (Throwable t2) {
                computerName = "";
            }

            //**************************************************************** 
            //other initialization

            //trigger CfToGcmd initialization to ensure CfToGcmd.txt file is valid.
            String testCfToGcmd[] = CfToFromGcmd.cfToGcmd("sea_water_temperature");
            Test.ensureTrue(testCfToGcmd.length > 0, "testCfToGcmd=" + String2.toCSSVString(testCfToGcmd));

            //successfully finished
            String2.log("*** " + erdStartup + " finished successfully." + eol);

        } catch (Throwable t) {
            errorInMethod = "ERROR during " + erdStartup + ":\n" + errorInMethod + "\n"
                    + MustBe.throwableToString(t);
            System.out.println(errorInMethod);
            //        if (String2.logFileName() != null) 
            //            String2.log(errorInMethod);
            //        String2.returnLoggingToSystemOut();
            throw new RuntimeException(errorInMethod);
        }

    }

    /** 
     * If loggedInAs is null, this returns baseUrl, else baseHttpsUrl
     *  (neither has slash at end).
     *
     * @param loggedInAs
     * @return If loggedInAs == null, this returns baseUrl, else baseHttpsUrl
     *  (neither has slash at end).
     */
    public static String baseUrl(String loggedInAs) {
        return loggedInAs == null ? baseUrl : baseHttpsUrl; //works because of loggedInAsHttps
    }

    /** 
     * If loggedInAs is null, this returns erddapUrl, else erddapHttpsUrl
     *  (neither has slash at end).
     *
     * @param loggedInAs
     * @return If loggedInAs == null, this returns erddapUrl, else erddapHttpsUrl
     *  (neither has slash at end).
     */
    public static String erddapUrl(String loggedInAs) {
        return loggedInAs == null ? erddapUrl : erddapHttpsUrl; //works because of loggedInAsHttps
    }

    /** 
     * If loggedInAs is null, this returns imageDirUrl, else imageDirHttpsUrl
     *  (with slash at end).
     *
     * @param loggedInAs
     * @return If loggedInAs == null, this returns imageDirUrl, else imageDirHttpsUrl
     *  (with slash at end).
     */
    public static String imageDirUrl(String loggedInAs) {
        return loggedInAs == null ? imageDirUrl : imageDirHttpsUrl; //works because of loggedInAsHttps
    }

    /** 
     * This returns the html needed to display the external.png image 
     * with the warning that the link is to an external web site.
     *
     * @param tErddapUrl
     * @return the html needed to display the external.png image and messages.
     */
    public static String externalLinkHtml(String tErddapUrl) {
        return "<img\n" + "    src=\"" + tErddapUrl + "/images/external.png\" " + "alt=\"" + externalLink + "\"\n"
                + "    title=\"" + externalWebSite + "\">";
    }

    /**
     * This is used by html web page generating methods to 
     * return the You Are Here html for ERDDAP.
     * 
     * @return the You Are Here html for this EDD subclass.
     */
    public static String youAreHere() {
        return "\n<h1>" + ProgramName + "</h1>\n";
    }

    /**
     * This is used by html web page generating methods to 
     * return the You Are Here html for a ERDDAP/protocol.
     * 
     * @param loggedInAs 
     * @param protocol e.g., tabledap
     * @return the You Are Here html for this EDD subclass.
     */
    public static String youAreHere(String loggedInAs, String protocol) {
        return "\n<h1 class=\"nowrap\">" + erddapHref(erddapUrl(loggedInAs)) + " &gt; " + protocol + "</h1>\n";
    }

    /** This returns a not-yet-HTML-encoded protocol URL.
     * You may want to encode it with XML.encodeAsHTML(url)
     */
    public static String protocolUrl(String tErddapUrl, String protocol) {
        return tErddapUrl + "/" + protocol + (protocol.equals("files") ? "/" : "/index.html")
                + (protocol.equals("tabledap") || protocol.equals("griddap") || protocol.equals("wms")
                        || protocol.equals("wcs") || protocol.equals("info") || protocol.equals("categorize")
                                ? "?" + defaultPIppQuery
                                : "");
    }

    /**
     * This is used by html web page generating methods to 
     * return the You Are Here html for ERDDAP/protocol/datasetID.
     * 
     * @param loggedInAs
     * @param protocol e.g., tabledap  (must be the same case as in the URL so the link will work)
     * @param datasetID e.g., erdGlobecBottle
     * @return the You Are Here html for this EDD subclass.
     */
    public static String youAreHere(String loggedInAs, String protocol, String datasetID) {
        String tErddapUrl = erddapUrl(loggedInAs);
        return "\n<h1 class=\"nowrap\">" + erddapHref(tErddapUrl) + "\n &gt; <a rel=\"contents\" " + "href=\""
                + XML.encodeAsHTMLAttribute(protocolUrl(tErddapUrl, protocol)) + "\">" + protocol + "</a>"
                + "\n &gt; " + datasetID + "</h1>\n";
    }

    /**
     * This is used by html web page generating methods to 
     * return the You Are Here html for ERDDAP/protocol with a helpful information.
     * 
     * @param loggedInAs
     * @param protocol e.g., tabledap
     * @param htmlHelp 
     * @return the You Are Here html for this EDD subclass.
     */
    public static String youAreHereWithHelp(String loggedInAs, String protocol, String htmlHelp) {
        String tErddapUrl = erddapUrl(loggedInAs);
        return "\n<h1 class=\"nowrap\">" + erddapHref(tErddapUrl) + "\n &gt; " + protocol + "\n"
                + htmlTooltipImage(loggedInAs, htmlHelp) + "\n</h1>\n";
    }

    /**
     * This is used by html web page generating methods to 
     * return the You Are Here html for ERDDAP/protocol/datasetID with a helpful information.
     * 
     * @param loggedInAs
     * @param protocol e.g., tabledap
     * @param datasetID e.g., erdGlobecBottle
     * @param htmlHelp 
     * @return the You Are Here html for this EDD subclass.
     */
    public static String youAreHereWithHelp(String loggedInAs, String protocol, String datasetID, String htmlHelp) {

        String tErddapUrl = erddapUrl(loggedInAs);
        return "\n<h1 class=\"nowrap\">" + erddapHref(tErddapUrl) + "\n &gt; <a rel=\"contents\" " + "href=\""
                + XML.encodeAsHTMLAttribute(protocolUrl(tErddapUrl, protocol)) + "\">" + protocol + "</a>"
                + "\n &gt; " + datasetID + "\n" + htmlTooltipImage(loggedInAs, htmlHelp) + "\n</h1>\n";
    }

    /** THIS IS NO LONGER USED
     * This is used by html web page generating methods to 
     * return the You Are Here html for ERDDAP/{protocol}/{attribute}/{category}.
     * IF REVIVED, append current ?page=x&amp;itemsPerPage=y
     * 
     * @param loggedInAs
     * @param protocol e.g., categorize
     * @param attribute e.g., ioos_category
     * @param category e.g., Temperature
     * @return the You Are Here html for this EDD subclass.
     */
    /*public static String youAreHere(String loggedInAs, String protocol, 
    String attribute, String category) {
        
    String tErddapUrl = erddapUrl(loggedInAs);
    String attributeUrl = tErddapUrl + "/" + protocol + "/" + attribute + "/index.html"; //+?defaultPIppQuery
    return 
        "\n<h1>" + erddapHref(tErddapUrl) + 
        "\n &gt; <a href=\"" + XML.encodeAsHTMLAttribute(protocolUrl(tErddapUrl, protocol)) + 
            "\">" + protocol + "</a>" +
        "\n &gt; <a href=\"" + XML.encodeAsHTMLAttribute(attributeUrl) + "\">" + attribute + "</a>" +
        "\n &gt; " + category + 
        "\n</h1>\n";
    }*/

    /**
     * This returns the html to draw a question mark that has big html tooltip.
     * htmlTooltipScript (see HtmlWidgets) must be already in the document.
     *
     * @param html  the html tooltip text, e.g., "Hi,<br>there!".
     *     It needs explicit br tags to set window width correctly.
     *     For plain text, generate html from XML.encodeAsPreHTML(plainText, 82).
     */
    public static String htmlTooltipImage(String loggedInAs, String html) {
        return HtmlWidgets.htmlTooltipImage(imageDirUrl(loggedInAs) + questionMarkImageFile, "?", html, "");
    }

    /**
     * This returns the html to draw a question mark that has big html tooltip
     * for an EDDTable EDV data variable.
     *
     * @param edv from an EDDTable
     */
    public static String htmlTooltipImageEDV(String loggedInAs, EDV edv) throws Throwable {

        return htmlTooltipImageLowEDV(loggedInAs, edv.destinationDataTypeClass(), edv.destinationName(),
                edv.combinedAttributes());
    }

    /**
     * This returns the html to draw a question mark that has big html tooltip 
     * for an EDDGrid EDV axis variable.
     *
     * @param edvga
     */
    public static String htmlTooltipImageEDVGA(String loggedInAs, EDVGridAxis edvga) throws Throwable {

        return htmlTooltipImageLowEDV(loggedInAs, edvga.destinationDataTypeClass(),
                edvga.destinationName() + "[" + edvga.sourceValues().size() + "]", edvga.combinedAttributes());
    }

    /**
     * This returns the html to draw a question mark that has big html tooltip 
     * for an EDDGrid EDV data variable.
     *
     * @param edv for a grid variable
     * @param allDimString  from eddGrid.allDimString()
     */
    public static String htmlTooltipImageEDVG(String loggedInAs, EDV edv, String allDimString) throws Throwable {

        return htmlTooltipImageLowEDV(loggedInAs, edv.destinationDataTypeClass(),
                edv.destinationName() + allDimString, edv.combinedAttributes());
    }

    /**
     * This returns the html to draw a question mark that has big html tooltip
     * with a variable's name and attributes.
     * htmlTooltipScript (see HtmlWidgets) must be already in the document.
     *
     * @param destinationDataTypeClass
     * @param destinationName  perhaps with axis information appended (e.g., [time][latitude][longitude]
     * @param attributes
     */
    public static String htmlTooltipImageLowEDV(String loggedInAs, Class destinationDataTypeClass,
            String destinationName, Attributes attributes) throws Throwable {

        String destType =
                //long and char aren't handled by getAtomicType. I don't think ever used.
                destinationDataTypeClass == long.class ? "long" : destinationDataTypeClass == char.class ? "char" : //???   
                        OpendapHelper.getAtomicType(destinationDataTypeClass);

        StringBuilder sb = OpendapHelper.dasToStringBuilder(destType + " " + destinationName, attributes, false); //false, do encoding below
        //String2.log("htmlTooltipImage sb=" + sb.toString());
        return htmlTooltipImage(loggedInAs,
                "<div class=\"standard_max_width\">" + XML.encodeAsPreHTML(sb.toString()) + "</div>");
    }

    /**
     * This sends the specified email to one or more emailAddresses.
     *
     * @param emailAddressCsv  comma-separated list (may have ", ")
     * @return an error message ("" if no error).
     *     If emailAddress is null or "", this logs the message and returns "".
     */
    public static String email(String emailAddressCsv, String subject, String content) {
        return email(String2.split(emailAddressCsv, ','), subject, content);
    }

    /**
     * This sends the specified email to the emailAddresses.
     * <br>This won't throw an exception if trouble.
     * <br>This method always prepends the subject and content with [erddapUrl],
     *   so that it will be clear which ERDDAP this came from 
     *   (in case you administer multiple ERDDAPs).
     * <br>This method always logs that an email was sent: to whom and the subject, 
     *   but not the content.
     * <br>This method logs all emails to the email log, e.g., 
     *   (bigParentDirectory)/emailLog2009-01.txt
     *
     * @param emailAddresses   each e.g., john.doe@company.com
     * @param subject If error, recommended: "Error in [someClass]".
     *   If this starts with EDStatic.DONT_LOG_THIS_EMAIL, this email won't be logged
     *   (which is useful for confidential emails).
     * @param content If error, recommended: MustBe.throwableToString(t);
     * @return an error message ("" if no error).
     *     If emailAddresses is null or length==0, this logs the message and returns "".
     */
    public static String email(String emailAddresses[], String subject, String content) {

        //write the email to the log
        String emailAddressesCSSV = String2.toCSSVString(emailAddresses);
        String localTime = Calendar2.getCurrentISODateTimeStringLocalTZ();
        boolean logIt = !subject.startsWith(DONT_LOG_THIS_EMAIL);
        if (!logIt)
            subject = subject.substring(DONT_LOG_THIS_EMAIL.length());
        subject = (computerName.length() > 0 ? computerName + " " : "") + "ERDDAP: "
                + String2.replaceAll(subject, '\n', ' ');

        //almost always write to emailLog
        try {
            //Always note that email sent in regular log.
            String2.log("Emailing \"" + subject + "\" to " + emailAddressesCSSV);

            String date = localTime.substring(0, 10);
            if (!emailLogDate.equals(date) || emailLogFile == null) {
                //update emailLogDate
                //do first so other threads won't do this simultaneously
                emailLogDate = date;

                //close the previous file
                if (emailLogFile != null) {
                    try {
                        emailLogFile.close();
                    } catch (Throwable t) {
                    }
                    emailLogFile = null;
                }

                //open a new file
                emailLogFile = new BufferedWriter(
                        new FileWriter(fullLogsDirectory + "emailLog" + date + ".txt", true)); //true=append
            }

            //write the email to the log
            //do in one write encourages threads not to intermingle   (or synchronize on emailLogFile?)
            emailLogFile.write("\n==== BEGIN ====================================================================="
                    + "\n     To: " + emailAddressesCSSV + "\nSubject: " + subject + //always non-https url
                    "\n   Date: " + localTime
                    + "\n--------------------------------------------------------------------------------"
                    + (logIt ? "\n" + erddapUrl + " reports:" + //always non-https url
                            "\n" + content : "\n[CONFIDENTIAL]")
                    + "\n==== END =======================================================================" + "\n");
            emailLogFile.flush();

        } catch (Throwable t) {
            try {
                String2.log(MustBe.throwable("Error: Writing to emailLog failed.", t));
            } catch (Throwable t2) {
            }
            if (emailLogFile != null) {
                try {
                    emailLogFile.close();
                } catch (Throwable t3) {
                }
                emailLogFile = null;
            }
        }

        //done?
        if (emailAddressesCSSV == null || emailAddressesCSSV.length() == 0 || emailSmtpHost == null
                || emailSmtpHost.length() == 0)
            return "";

        //send email
        String errors = "";
        try {
            //catch common problem: sending email to one invalid address
            if (emailAddresses.length == 1 && (!String2.isEmailAddress(emailAddresses[0])
                    || emailAddresses[0].startsWith("nobody@") || emailAddresses[0].startsWith("your.name")
                    || emailAddresses[0].startsWith("your.email"))) {
                errors = "Error in EDStatic.email: invalid emailAddresses=" + emailAddressesCSSV;
                String2.log(errors);
            }

            if (errors.length() == 0)
                //??? THREAD SAFE? SYNCHRONIZED? 
                //I don't think use of this needs to be synchronized. I could be wrong. I haven't tested.
                SSR.sendEmail(emailSmtpHost, emailSmtpPort, emailUserName, emailPassword, emailProperties,
                        emailFromAddress, emailAddressesCSSV, subject, erddapUrl + " reports:\n" + content); //always non-https url
        } catch (Throwable t) {
            String msg = "Error: Sending email to " + emailAddressesCSSV + " failed";
            try {
                String2.log(MustBe.throwable(msg, t));
            } catch (Throwable t4) {
            }
            errors = msg + ": " + t.toString() + "\n";
        }

        //write errors to email log
        if (errors.length() > 0 && emailLogFile != null) {
            try {
                //do in one write encourages threads not to intermingle   (or synchronize on emailLogFile?)
                emailLogFile.write("\n********** ERRORS **********\n" + errors);
                emailLogFile.flush();

            } catch (Throwable t) {
                String2.log(MustBe.throwable("Error: Writing to emailLog failed.", t));
                if (emailLogFile != null) {
                    try {
                        emailLogFile.close();
                    } catch (Throwable t2) {
                    }
                    emailLogFile = null;
                }
            }
        }

        return errors;
    }

    /**
     * This throws an exception if the requested nBytes are unlikely to be
     * available.
     * This isn't perfect, but is better than nothing. 
     * Future: locks? synchronization? ...?
     *
     * <p>This is almost identical to Math2.ensureMemoryAvailable, but adds tallying.
     *
     * @param nBytes  size of data structure that caller plans to create
     * @param attributeTo for the Tally system, this is the string (datasetID?) 
     *   to which this not-enough-memory issue should be attributed.
     * @throws RuntimeException if the requested nBytes are unlikely to be available.
     */
    public static void ensureMemoryAvailable(long nBytes, String attributeTo) {

        //if it is a small request, don't take the time/effort to check 
        if (nBytes < Math2.ensureMemoryAvailableTrigger)
            return;
        String attributeToParen = attributeTo == null || attributeTo.length() == 0 ? "" : " (" + attributeTo + ")";

        //is the request too big under any circumstances?
        if (nBytes > Math2.maxSafeMemory) {
            tally.add("Request refused: not enough memory ever (since startup)", attributeTo);
            tally.add("Request refused: not enough memory ever (since last daily report)", attributeTo);
            throw new RuntimeException(Math2.memoryTooMuchData + "  " + MessageFormat.format(Math2.memoryThanSafe,
                    "" + (nBytes / Math2.BytesPerMB), "" + (Math2.maxSafeMemory / Math2.BytesPerMB))
                    + attributeToParen);
        }

        //request is fine without gc?
        long memoryInUse = Math2.getMemoryInUse();
        if (memoryInUse + nBytes <= Math2.maxSafeMemory) //it'll work
            return;

        //lots of memory is in use
        //is the request is too big for right now?
        Math2.gcAndWait(); //part of ensureMemoryAvailable: lots of memory in use
        memoryInUse = Math2.getMemoryInUse();
        if (memoryInUse + nBytes > Math2.maxSafeMemory) {
            //eek! not enough memory! 
            //Wait, then try gc again and hope that some other request requiring lots of memory will finish.
            //If nothing else, this 1 second delay will delay another request by same user (e.g., programmatic re-request)
            Math2.sleep(1000);
            Math2.gcAndWait(); //in ensureMemoryIsAvailable, lots of memory in use
            memoryInUse = Math2.getMemoryInUse();
        }
        if (memoryInUse > Math2.maxSafeMemory) {
            tally.add("MemoryInUse > MaxSafeMemory (since startup)", attributeTo);
            tally.add("MemoryInUse > MaxSafeMemory (since last daily report)", attributeTo);
        }
        if (memoryInUse + nBytes > Math2.maxSafeMemory) {
            tally.add("Request refused: not enough memory currently (since startup)", attributeTo);
            tally.add("Request refused: not enough memory currently (since last daily report)", attributeTo);
            throw new RuntimeException(
                    Math2.memoryTooMuchData + "  "
                            + MessageFormat.format(Math2.memoryThanCurrentlySafe, "" + (nBytes / Math2.BytesPerMB),
                                    "" + ((Math2.maxSafeMemory - memoryInUse) / Math2.BytesPerMB))
                            + attributeToParen);
        }
    }

    /** 
     * Even if JavaBits is 64, the limit on an array size is Integer.MAX_VALUE.
     *
     * <p>This is almost identical to Math2.ensureArraySizeOkay, but adds tallying.
     * 
     * @param tSize
     * @param attributeTo for the Tally system, this is the string (datasetID?) 
     *   to which this not-enough-memory issue should be attributed.
     * @throws Exception if tSize >= Integer.MAX_VALUE.  
     *  (equals is forbidden for safety since I often use if as missing value / trouble)
     */
    public static void ensureArraySizeOkay(long tSize, String attributeTo) {
        if (tSize >= Integer.MAX_VALUE) {
            tally.add("Request refused: array size >= Integer.MAX_VALUE (since startup)", attributeTo);
            tally.add("Request refused: array size >= Integer.MAX_VALUE (since last daily report)", attributeTo);
            throw new RuntimeException(Math2.memoryTooMuchData + "  "
                    + MessageFormat.format(Math2.memoryArraySize, "" + tSize, "" + Integer.MAX_VALUE)
                    + (attributeTo == null || attributeTo.length() == 0 ? "" : " (" + attributeTo + ")"));
        }
    }

    /**
     * This sets the request blacklist of numeric ip addresses (e.g., 123.45.67.89)
     * (e.g., to fend of a Denial of Service attack or an overzealous web robot).
     * This sets requestBlacklist to be a HashSet (or null).
     *
     * @param csv the comma separated list of numeric ip addresses
     */
    public static void setRequestBlacklist(String csv) {
        if (csv == null || csv.length() == 0) {
            requestBlacklist = null;
            String2.log("requestBlacklist is now null.");
        } else {
            String rb[] = String2.split(csv, ',');
            HashSet hs = new HashSet(Math2.roundToInt(1.4 * rb.length));
            for (int i = 0; i < rb.length; i++)
                hs.add(rb[i]);
            requestBlacklist = hs; //set in an instant
            String2.log("requestBlacklist is now " + String2.toCSSVString(rb));
        }
    }

    /**
     * This adds the common, publicly accessible statistics to the StringBuilder.
     */
    public static void addIntroStatistics(StringBuilder sb) {
        sb.append("Current time is " + Calendar2.getCurrentISODateTimeStringLocalTZ() + "\n");
        sb.append("Startup was at  " + startupLocalDateTime + "\n");
        long loadTime = lastMajorLoadDatasetsStopTimeMillis - lastMajorLoadDatasetsStartTimeMillis;
        sb.append("Last major LoadDatasets started " + Calendar2.elapsedTimeString(
                1000 * Math2.roundToInt((System.currentTimeMillis() - lastMajorLoadDatasetsStartTimeMillis) / 1000))
                + " ago and "
                + (loadTime < 0 ? "is still running.\n" : "finished after " + (loadTime / 1000) + " seconds.\n"));
        //would be nice to know if minor LoadDataset is active, for how long
        sb.append("nGridDatasets  = " + nGridDatasets + "\n");
        sb.append("nTableDatasets = " + nTableDatasets + "\n");
        sb.append("nTotalDatasets = " + (nGridDatasets + nTableDatasets) + "\n");
        sb.append(datasetsThatFailedToLoad);
        sb.append(errorsDuringMajorReload);
        sb.append("Response Failed    Time (since last major LoadDatasets) ");
        sb.append(String2.getBriefDistributionStatistics(failureTimesDistributionLoadDatasets) + "\n");
        sb.append("Response Failed    Time (since last Daily Report)       ");
        sb.append(String2.getBriefDistributionStatistics(failureTimesDistribution24) + "\n");
        sb.append("Response Failed    Time (since startup)                 ");
        sb.append(String2.getBriefDistributionStatistics(failureTimesDistributionTotal) + "\n");
        sb.append("Response Succeeded Time (since last major LoadDatasets) ");
        sb.append(String2.getBriefDistributionStatistics(responseTimesDistributionLoadDatasets) + "\n");
        sb.append("Response Succeeded Time (since last Daily Report)       ");
        sb.append(String2.getBriefDistributionStatistics(responseTimesDistribution24) + "\n");
        sb.append("Response Succeeded Time (since startup)                 ");
        sb.append(String2.getBriefDistributionStatistics(responseTimesDistributionTotal) + "\n");

        synchronized (taskList) { //all task-related things synch on taskList
            ensureTaskThreadIsRunningIfNeeded(); //clients (like this class) are responsible for checking on it
            long tElapsedTime = taskThread == null ? -1 : taskThread.elapsedTime();
            sb.append(
                    "TaskThread has finished " + (lastFinishedTask + 1) + " out of " + taskList.size() + " tasks.  "
                            + (tElapsedTime < 0 ? "Currently, no task is running.\n"
                                    : "The current task has been running for "
                                            + Calendar2.elapsedTimeString(tElapsedTime) + ".\n"));
        }
        sb.append("TaskThread Failed    Time (since last Daily Report)     ");
        sb.append(String2.getBriefDistributionStatistics(taskThreadFailedDistribution24) + "\n");
        sb.append("TaskThread Failed    Time (since startup)               ");
        sb.append(String2.getBriefDistributionStatistics(taskThreadFailedDistributionTotal) + "\n");
        sb.append("TaskThread Succeeded Time (since last Daily Report)     ");
        sb.append(String2.getBriefDistributionStatistics(taskThreadSucceededDistribution24) + "\n");
        sb.append("TaskThread Succeeded Time (since startup)               ");
        sb.append(String2.getBriefDistributionStatistics(taskThreadSucceededDistributionTotal) + "\n");
    }

    /**
     * This adds the common, publicly accessible statistics to the StringBuffer.
     */
    public static void addCommonStatistics(StringBuilder sb) {
        if (majorLoadDatasetsTimeSeriesSB.length() > 0) {
            sb.append(
                    "Major LoadDatasets Time Series: MLD    Datasets Loaded    Requests (medianTime in seconds)     Number of Threads      Memory (MB)\n"
                            + "  timestamp                    time   nTry nFail nTotal  nSuccess (median) nFailed (median)  tomWait inotify other  inUse highWater\n");
            sb.append(majorLoadDatasetsTimeSeriesSB);
            sb.append("\n\n");
        }

        sb.append("Major LoadDatasets Times Distribution (since last Daily Report):\n");
        sb.append(String2.getDistributionStatistics(majorLoadDatasetsDistribution24));
        sb.append('\n');
        sb.append("Major LoadDatasets Times Distribution (since startup):\n");
        sb.append(String2.getDistributionStatistics(majorLoadDatasetsDistributionTotal));
        sb.append('\n');
        sb.append('\n');

        sb.append("Minor LoadDatasets Times Distribution (since last Daily Report):\n");
        sb.append(String2.getDistributionStatistics(minorLoadDatasetsDistribution24));
        sb.append('\n');
        sb.append("Minor LoadDatasets Times Distribution (since startup):\n");
        sb.append(String2.getDistributionStatistics(minorLoadDatasetsDistributionTotal));
        sb.append('\n');
        sb.append('\n');

        sb.append("Response Failed Time Distribution (since last major LoadDatasets):\n");
        sb.append(String2.getDistributionStatistics(failureTimesDistributionLoadDatasets));
        sb.append('\n');
        sb.append("Response Failed Time Distribution (since last Daily Report):\n");
        sb.append(String2.getDistributionStatistics(failureTimesDistribution24));
        sb.append('\n');
        sb.append("Response Failed Time Distribution (since startup):\n");
        sb.append(String2.getDistributionStatistics(failureTimesDistributionTotal));
        sb.append('\n');
        sb.append('\n');

        sb.append("Response Succeeded Time Distribution (since last major LoadDatasets):\n");
        sb.append(String2.getDistributionStatistics(responseTimesDistributionLoadDatasets));
        sb.append('\n');
        sb.append("Response Succeeded Time Distribution (since last Daily Report):\n");
        sb.append(String2.getDistributionStatistics(responseTimesDistribution24));
        sb.append('\n');
        sb.append("Response Succeeded Time Distribution (since startup):\n");
        sb.append(String2.getDistributionStatistics(responseTimesDistributionTotal));
        sb.append('\n');
        sb.append('\n');

        sb.append("TaskThread Failed Time Distribution (since last Daily Report):\n");
        sb.append(String2.getDistributionStatistics(taskThreadFailedDistribution24));
        sb.append('\n');
        sb.append("TaskThread Failed Time Distribution (since startup):\n");
        sb.append(String2.getDistributionStatistics(taskThreadFailedDistributionTotal));
        sb.append('\n');
        sb.append("TaskThread Succeeded Time Distribution (since last Daily Report):\n");
        sb.append(String2.getDistributionStatistics(taskThreadSucceededDistribution24));
        sb.append('\n');
        sb.append("TaskThread Succeeded Time Distribution (since startup):\n");
        sb.append(String2.getDistributionStatistics(taskThreadSucceededDistributionTotal));
        sb.append('\n');
        sb.append('\n');

        sb.append(SgtMap.topographyStats() + "\n");
        sb.append(GSHHS.statsString() + "\n");
        sb.append(SgtMap.nationalBoundaries.statsString() + "\n");
        sb.append(SgtMap.stateBoundaries.statsString() + "\n");
        sb.append(SgtMap.rivers.statsString() + "\n");
        sb.append(SgtUtil.isBufferedImageAccelerated() + "\n");
        sb.append('\n');

    }

    /** 
     * This returns the user's login name (or null if not logged in).
     *
     * <p>This relies on EDStatic.authentication
     *
     * <p>This is safe to use this after outputStream has been written to -- 
     *     this won't make a session if the user doesn't have one.
     *
     * @param request
     * @return null (using http), 
     *    loggedInAsHttps (using https and not logged in), 
     *    or userName (using https and logged in).
     */
    public static String getLoggedInAs(HttpServletRequest request) {
        if (request == null)
            return null;

        //request is via http? treat as not logged in
        String fullRequestUrl = request.getRequestURL().toString(); //has proxied port#, e.g. :8080
        if (!fullRequestUrl.startsWith("https://"))
            return null;

        //request is via https, but authentication=""?
        if (authentication.length() == 0)
            return loggedInAsHttps;

        //see if user is logged in
        //NOTE: session is associated with https urls, not http urls!
        //  So user only appears logged in to https urls.
        HttpSession session = request.getSession(false); //don't make one if none already
        //String2.log("session=" + (session==null? "null" : session.getServletContext().getServletContextName()));

        if (session == null)
            return loggedInAsHttps;

        //session != null
        String loggedInAs = null;
        if (authentication.equals("custom") || authentication.equals("email") || authentication.equals("google")) {
            loggedInAs = (String) session.getAttribute("loggedInAs:" + warName);

            //} else if (authentication.equals("openid")) 
            //    loggedInAs = OpenIdFilter.getCurrentUser(session);
        }

        //ensure printable characters only (which makes loggedInAsSuperuser special)
        return loggedInAs == null ? loggedInAsHttps : String2.justPrintable(loggedInAs);
    }

    /**
     * This generates a nonce (a long "random" string related to basis).
     */
    public static String nonce(String basis) {
        return String2.passwordDigest("SHA-256", Math2.random(Integer.MAX_VALUE) + "_" + System.currentTimeMillis()
                + "_" + basis + "_" + flagKeyKey);
    }

    /** This allows LoadDatasets to set EDStatic.userHashMap (which is private).
     * There is no getUserHashMap (so info remains private).
     * MD5'd and SHA256'd passwords should all already be lowercase.
     */
    public static void setUserHashMap(HashMap tUserHashMap) {
        userHashMap = tUserHashMap;
    }

    /**
     * This returns true if the plaintextPassword (after passwordEncoding as 
     * specified in setup.xml) matches the stored password for user.
     *
     * @param username the user's log in name
     * @param plaintextPassword that the user entered on a log-in form
     * @return true if the plaintextPassword (after passwordEncoding as 
     *    specified in setup.xml) matches the stored password for username.
     *    If user==null or user has no password defined in datasets.xml, this returns false.
     */
    public static boolean doesPasswordMatch(String username, String plaintextPassword) {
        if (username == null || username.length() == 0 || !username.equals(String2.justPrintable(username))
                || plaintextPassword == null || plaintextPassword.length() < minimumPasswordLength)
            return false;

        Object oar[] = (Object[]) userHashMap.get(username);
        if (oar == null) {
            String2.log("username=" + username + " not found in userHashMap.");
            return false;
        }
        String expected = (String) oar[0]; //using passwordEncoding in setup.xml
        if (expected == null)
            return false;

        //generate observedPassword from plaintextPassword via passwordEncoding 
        String observed = plaintextPassword;
        if (passwordEncoding.equals("MD5"))
            observed = String2.md5Hex(plaintextPassword); //it will be lowercase
        else if (passwordEncoding.equals("UEPMD5"))
            observed = String2.md5Hex(username + ":ERDDAP:" + plaintextPassword); //it will be lowercase
        else if (passwordEncoding.equals("SHA256"))
            observed = String2.passwordDigest("SHA-256", plaintextPassword); //it will be lowercase
        else if (passwordEncoding.equals("UEPSHA256"))
            observed = String2.passwordDigest("SHA-256", username + ":ERDDAP:" + plaintextPassword); //it will be lowercase
        else
            throw new RuntimeException("Unexpected passwordEncoding=" + passwordEncoding);
        //only for debugging:
        //String2.log("username=" + username +
        //    "\nobsPassword=" + observed +
        //    "\nexpPassword=" + expected);

        boolean ok = observed.equals(expected);
        if (reallyVerbose)
            String2.log("username=" + username + " password matched: " + ok);
        return ok;
    }

    /**
     * This indicates if a user is on the list of potential users 
     * (i.e., there's a user tag for this user in datasets.xml).
     *
     * @param userName the user's potential user name 
     * @return true if a user is on the list of potential users
     * (i.e., there's a user tag for this user in datasets.xml).
     */
    public static boolean onListOfUsers(String userName) {
        if (!String2.isSomething(userName))
            return false;
        return userHashMap.get(userName) != null;
    }

    /**
     * This returns the roles for a user.
     *
     * @param loggedInAs the user's logged in name (or null if not logged in)
     * @return the roles for the user.
     *    If user==null or user has no roles defined in datasets.xml, this returns null.
     */
    public static String[] getRoles(String loggedInAs) {
        if (loggedInAs == null)
            return null;

        //???future: for authentication="basic", use tomcat-defined roles???

        //all other authentication methods
        Object oar[] = (Object[]) userHashMap.get(loggedInAs);
        if (oar == null)
            return null;
        return (String[]) oar[1];
    }

    /**
     * If the user tries to access a dataset to which he doesn't have access,
     * call this to send Http UNAUTHORIZED error.
     * (was: redirectToLogin: redirect him to the login page).
     *
     * @param loggedInAs  the name of the logged in user (or null if not logged in)
     * @param datasetID  or use "" for general login.
     * @param graphsAccessibleToPublic From edd.graphsAccessibleToPublic(). 
     *   If this is true, then this method
     *   was called because the request was for data from a dataset that
     *   allows graphics|metadata requests from the public.
     * @throws Throwable (notably ClientAbortException)
     */
    public static void sendHttpUnauthorizedError(String loggedInAs, HttpServletResponse response, String datasetID,
            boolean graphsAccessibleToPublic) throws Throwable {

        String message = null;
        try {
            tally.add("Request refused: not authorized (since startup)", datasetID);
            tally.add("Request refused: not authorized (since last daily report)", datasetID);

            if (datasetID != null && datasetID.length() > 0)
                message = MessageFormat.format(
                        graphsAccessibleToPublic ? EDStatic.notAuthorizedForData : EDStatic.notAuthorized,
                        loggedInAsHttps.equals(loggedInAs) ? "" : loggedInAs, datasetID);

            if (message == null)
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            else
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, message);

        } catch (Throwable t2) {
            EDStatic.rethrowClientAbortException(t2); //first thing in catch{}
            String2.log("Error in sendHttpUnauthorizedError:\n" + (message == null ? "" : message + "\n")
                    + MustBe.throwableToString(t2));
        }
    }

    /** 
     * This returns the session's login status html (with a link to log in/out)
     * suitable for use at the top of a web page.
     * <br>If logged in: loggedInAs | logout
     * <br>If not logged in:  login
     *
     * <p>This is safe to use this after outputStream has been written to -- 
     *     this won't make a session if the user doesn't have one.
     *
     * @param loggedInAs  the name of the logged in user (or null if not logged in)
     *   Special case: "loggedInAsHttps" is for using https without being logged in, 
     *   but &amp;loginInfo; indicates user isn't logged in.
     */
    public static String getLoginHtml(String loggedInAs) {
        if (authentication.equals("")) {
            //user can't log in
            return "";
        } else {
            return loggedInAs == null || loggedInAsHttps.equals(loggedInAs) ? //ie not logged in
            //always use the erddapHttpsUrl for login/logout pages
                    "<a href=\"" + erddapHttpsUrl + "/login.html\">" + login + "</a>"
                    : "<a href=\"" + erddapHttpsUrl + "/login.html\"><strong>" + XML.encodeAsHTML(loggedInAs)
                            + "</strong></a> | \n" + "<a href=\"" + erddapHttpsUrl + "/logout.html\">" + logout
                            + "</a>";
        }
    }

    /**
     * This returns the startBodyHtml (with the user's login info inserted if 
     * &amp;loginInfo; is present).
     *
     * <p>This is safe to use this after outputStream has been written to -- 
     *     this won't make a session if the user doesn't have one.
     *
     * @param loggedInAs  the name of the logged in user (or null if not logged in).
     *   Special case: "loggedInAsHttps" is for using https without being logged in, 
     *   but &amp;loginInfo; indicates user isn't logged in.
     */
    public static String startBodyHtml(String loggedInAs) {
        String s = startBodyHtml;
        if (ampLoginInfoPo >= 0) {
            s = startBodyHtml.substring(0, ampLoginInfoPo) + getLoginHtml(loggedInAs)
                    + startBodyHtml.substring(ampLoginInfoPo + ampLoginInfo.length());
        }
        return String2.replaceAll(s, "&erddapUrl;", erddapUrl(loggedInAs));
    }

    /**
     * @param tErddapUrl  from EDStatic.erddapUrl(loggedInAs)  (erddapUrl, or erddapHttpsUrl if user is logged in)
     */
    public static String endBodyHtml(String tErddapUrl) {
        return String2.replaceAll(endBodyHtml, "&erddapUrl;", tErddapUrl);
    }

    public static String legal(String tErddapUrl) {
        return String2.replaceAll(legal, "&erddapUrl;", tErddapUrl);
    }

    /** @param addToTitle has not yet been encodeAsHTML(addToTitle). */
    public static String startHeadHtml(String tErddapUrl, String addToTitle) {
        String ts = startHeadHtml;
        if (addToTitle.length() > 0)
            ts = String2.replaceAll(ts, "</title>", " - " + XML.encodeAsHTML(addToTitle) + "</title>");
        return String2.replaceAll(ts, "&erddapUrl;", tErddapUrl);
    }

    public static String theLongDescriptionHtml(String tErddapUrl) {
        return String2.replaceAll(theLongDescriptionHtml, "&erddapUrl;", tErddapUrl);
    }

    public static String theShortDescriptionHtml(String tErddapUrl) {
        return String2.replaceAll(theShortDescriptionHtml, "&erddapUrl;", tErddapUrl);
    }

    public static String erddapHref(String tErddapUrl) {
        return "<a title=\"" + clickERDDAP + "\" \n" + "rel=\"start\" " + "href=\"" + tErddapUrl + "/index.html\">"
                + ProgramName + "</a>";
    }

    /** This calls pEncode then hEncode. */
    public static String phEncode(String tUrl) {
        return hEncode(pEncode(tUrl));
    }

    /** This calls XML.encodeAsHTML(tUrl). */
    public static String hEncode(String tUrl) {
        return XML.encodeAsHTML(tUrl);
    }

    /** This percent encodes &lt; and &gt; */
    public static String pEncode(String tUrl) {
        tUrl = String2.replaceAll(tUrl, "<", "%3C");
        return String2.replaceAll(tUrl, ">", "%3E");
    }

    public static String adminContact() {
        String ae = String2.replaceAll(adminEmail, "@", " at ");
        ae = String2.replaceAll(ae, ".", " dot ");
        return adminIndividualName + " (email: " + ae + ")";
    }

    /**
     * This appends an error message to an html page and flushes the writer.
     * This also logs the error.
     *
     * <p>Note that the use of try/catch blocks and htmlForException is necessary
     * because the outputstream is usually gzipped, so erddap can't just 
     * catch the exception in doGet try/catch and append error message 
     * since original outputstream and writer (which own/control the gzip stream) 
     * aren't available.
     */
    public static String htmlForException(Throwable t) {
        String message = MustBe.getShortErrorMessage(t);
        String2.log("HtmlForException is processing:\n " + MustBe.throwableToString(t)); //log full message with stack trace
        return "<p>&nbsp;<hr>\n" + "<p><span class=\"warningColor\"><strong>" + errorOnWebPage
                + "</strong></span>\n" + "<pre>" + XML.encodeAsPreHTML(message, 100) + "</pre>\n";
    }

    /** This interrupts/kill all of the thredds in runningThreads. 
     *  Erddap.destroy calls this when tomcat is stopped.
     */
    public static void destroy() {
        long time = System.currentTimeMillis();
        try {
            String names[] = String2.toStringArray(runningThreads.keySet().toArray());
            String2.log("\nEDStatic.destroy will try to interrupt nThreads=" + names.length + "\n  threadNames="
                    + String2.toCSSVString(names));

            //shutdown Cassandra clusters/sessions
            EDDTableFromCassandra.shutdown();

            //interrupt all of them
            for (int i = 0; i < names.length; i++) {
                try {
                    Thread thread = (Thread) runningThreads.get(names[i]);
                    if (thread != null && thread.isAlive())
                        thread.interrupt();
                    else
                        runningThreads.remove(names[i]);
                } catch (Throwable t) {
                    String2.log(MustBe.throwableToString(t));
                }
            }

            //wait for threads to finish
            int waitedSeconds = 0;
            int maxSeconds = 600; //10 minutes
            while (true) {
                boolean allDone = true;
                for (int i = 0; i < names.length; i++) {
                    try {
                        if (names[i] == null)
                            continue; //it has already stopped
                        Thread thread = (Thread) runningThreads.get(names[i]);
                        if (thread != null && thread.isAlive()) {
                            allDone = false;
                            if (waitedSeconds > maxSeconds) {
                                String2.log("  " + names[i] + " thread is being stop()ped!!!");
                                thread.stop();
                                runningThreads.remove(names[i]);
                                names[i] = null;
                            }
                        } else {
                            String2.log("  " + names[i] + " thread recognized the interrupt in " + waitedSeconds
                                    + " s");
                            runningThreads.remove(names[i]);
                            names[i] = null;
                        }
                    } catch (Throwable t) {
                        String2.log(MustBe.throwableToString(t));
                        allDone = false;
                    }
                }
                if (allDone) {
                    String2.log("EDStatic.destroy successfully interrupted all threads in " + waitedSeconds + " s");
                    break;
                }
                if (waitedSeconds > maxSeconds) {
                    String2.log("!!! EDStatic.destroy is done, but it had to stop() some threads.");
                    break;
                }
                Math2.sleep(2000);
                waitedSeconds += 2;
            }

            //finally
            if (useLuceneSearchEngine)
                String2.log("stopping lucene...");

            try {
                if (luceneIndexSearcher != null)
                    luceneIndexSearcher.close();
            } catch (Throwable t) {
            }
            luceneIndexSearcher = null;

            try {
                if (luceneIndexReader != null)
                    luceneIndexReader.close();
            } catch (Throwable t) {
            }
            luceneIndexReader = null;
            luceneDatasetIDFieldCache = null;

            try {
                if (luceneIndexWriter != null)
                    //indices will be thrown away, so don't make pending changes
                    luceneIndexWriter.close(false);
            } catch (Throwable t) {
            }
            luceneIndexWriter = null;

        } catch (Throwable t) {
            String2.log(MustBe.throwableToString(t));
        }
    }

    /** This interrupts the thread and waits up to maxSeconds for it to finish.
     * If it still isn't finished, it is stopped.
     * 
     */
    public static void stopThread(Thread thread, int maxSeconds) {
        try {
            if (thread == null)
                return;
            String name = thread.getName();
            if (verbose)
                String2.log("stopThread(" + name + ")...");
            if (!thread.isAlive()) {
                if (verbose)
                    String2.log("thread=" + name + " was already not alive.");
                return;
            }
            thread.interrupt();
            int waitSeconds = 0;
            while (thread.isAlive() && waitSeconds < maxSeconds) {
                waitSeconds += 2;
                Math2.sleep(2000);
            }
            if (thread.isAlive()) {
                if (verbose)
                    String2.log("!!!Stopping thread=" + name + " after " + waitSeconds + " s");
                thread.stop();
            } else {
                if (verbose)
                    String2.log("thread=" + name + " noticed interrupt in " + waitSeconds + " s");
            }
        } catch (Throwable t) {
            String2.log(MustBe.throwableToString(t));
        }
    }

    /**
     * This checks if the task thread is running and not stalled.
     * If it is stalled, this will stop it.
     *
     * @return true if the task thread is running.
     *    If false, taskThread will be null.
     */
    public static boolean isTaskThreadRunning() {
        synchronized (taskList) { //all task-related things synch on taskList
            if (taskThread == null)
                return false;

            if (taskThread.isAlive()) {
                //is it stalled?
                long eTime = taskThread.elapsedTime();
                long maxTime = 6 * Calendar2.MILLIS_PER_HOUR; //appropriate??? user settable???
                if (eTime > maxTime) {

                    //taskThread is stalled; interrupt it
                    String tError = "\n*** Error: EDStatic is interrupting a stalled taskThread ("
                            + Calendar2.elapsedTimeString(eTime) + " > " + Calendar2.elapsedTimeString(maxTime)
                            + ") at " + Calendar2.getCurrentISODateTimeStringLocalTZ();
                    email(emailEverythingToCsv, "taskThread Stalled", tError);
                    String2.log("\n*** " + tError);

                    stopThread(taskThread, 10); //short time; it is already in trouble
                    //runningThreads.remove   not necessary since new one is put() in below
                    lastFinishedTask = nextTask - 1;
                    taskThread = null;
                    return false;
                }
                return true;
            } else {
                //it isn't alive
                String2.log("\n*** EDStatic noticed that taskThread is finished ("
                        + Calendar2.getCurrentISODateTimeStringLocalTZ() + ")\n");
                lastFinishedTask = nextTask - 1;
                taskThread = null;
                return false;
            }
        }
    }

    /** 
     * This ensures the task thread is running if there are tasks to do
     */
    public static void ensureTaskThreadIsRunningIfNeeded() {
        synchronized (taskList) { //all task-related things synch on taskList
            try {
                //this checks if it is running and not stalled
                if (isTaskThreadRunning())
                    return;

                //taskThread isn't running
                //Are there no tasks to do? 
                int nPending = taskList.size() - nextTask;
                if (nPending <= 0)
                    return; //no need to start it

                //need to start a new taskThread
                taskThread = new TaskThread(nextTask);
                runningThreads.put(taskThread.getName(), taskThread);
                String2.log("\n*** new taskThread started at " + Calendar2.getCurrentISODateTimeStringLocalTZ()
                        + " nPendingTasks=" + nPending + "\n");
                taskThread.start();
                return;
            } catch (Throwable t) {
            }
        }
    }

    /**
     * This returns the number of unfinished tasks.
     */
    public static int nUnfinishedTasks() {
        return (taskList.size() - lastFinishedTask) - 1;
    }

    /** This adds a task to the taskList if it (other than TASK_SET_FLAG)
     * isn't already on the taskList.
     * @return the task number that was assigned to the task,
     *   or -1 if it was a duplicate task.
     */
    public static int addTask(Object taskOA[]) {
        synchronized (taskList) { //all task-related things synch on taskList

            //Note that all task creators check that
            //   EDStatic.lastFinishedTask >= lastAssignedTask(datasetID).  I.E., tasks are all done,
            //before again creating new tasks.
            //So no need to see if this new task duplicates an existing unfinished task.  

            //add the task to the list
            taskList.add(taskOA);
            return taskList.size() - 1;
        }
    }

    /**
     * This returns the Oceanic/Atmospheric Acronyms table: col 0=acronym 1=fullName.
     * <br>Acronyms are case-sensitive, sometimes with common variants included.
     * <br>The table is basically sorted by acronym, but with longer acronyms
     *  (e.g., AMSRE) before shorter siblings (e.g., AMSR).
     * <br>Many of these are from
     * https://www.nodc.noaa.gov/General/mhdj_acronyms3.html
     * and http://www.psmsl.org/train_and_info/training/manuals/acronyms.html
     *
     * @return the oceanic/atmospheric acronyms table
     * @throws Exception if trouble (e.g., file not found)
     */
    public static Table oceanicAtmosphericAcronymsTable() throws Exception {
        Table table = new Table();
        StringArray col1 = new StringArray();
        StringArray col2 = new StringArray();
        table.addColumn("acronym", col1);
        table.addColumn("fullName", col2);
        String lines[] = String2.readLinesFromFile(
                contextDirectory + "WEB-INF/classes/gov/noaa/pfel/erddap/util/OceanicAtmosphericAcronyms.tsv",
                String2.ISO_8859_1, 1);
        int nLines = lines.length;
        for (int i = 1; i < nLines; i++) { //1 because skip colNames
            String s = lines[i].trim();
            if (s.length() == 0 || s.startsWith("//"))
                continue;
            int po = s.indexOf('\t');
            if (po < 0)
                po = s.length();
            col1.add(s.substring(0, po).trim());
            col2.add(s.substring(po + 1).trim());
        }
        return table;
    }

    /**
     * This returns the Oceanic/Atmospheric Variable Names table: col 0=variableName 1=fullName.
     * <br>varNames are all lower-case.  long_names are mostly first letter of each word capitalized.
     * <br>The table is basically sorted by varName.
     * <br>Many of these are from
     * https://www.esrl.noaa.gov/psd/data/gridded/conventions/variable_abbreviations.html
     *
     * @return the oceanic/atmospheric variable names table
     * @throws Exception if trouble (e.g., file not found)
     */
    public static Table oceanicAtmosphericVariableNamesTable() throws Exception {
        Table table = new Table();
        StringArray col1 = new StringArray();
        StringArray col2 = new StringArray();
        table.addColumn("variableName", col1);
        table.addColumn("fullName", col2);
        String lines[] = String2.readLinesFromFile(
                contextDirectory + "WEB-INF/classes/gov/noaa/pfel/erddap/util/OceanicAtmosphericVariableNames.tsv",
                String2.ISO_8859_1, 1);
        int nLines = lines.length;
        for (int i = 1; i < nLines; i++) {
            String s = lines[i].trim();
            if (s.length() == 0 || s.startsWith("//"))
                continue;
            int po = s.indexOf('\t');
            if (po < 0)
                po = s.length();
            col1.add(s.substring(0, po).trim());
            col2.add(s.substring(po + 1).trim());
        }
        return table;
    }

    /**
     * This returns the Oceanic/Atmospheric Acronyms table as a Table:
     * col0=acronym, col1=fullName.
     * THIS IS ONLY FOR GenerateDatasetsXml THREADS -- a few common acronyms are removed.
     *
     * @return the oceanic/atmospheric variable names table with some common acronyms removed
     * @throws Exception if trouble (e.g., file not found)
     */
    public static Table gdxAcronymsTable() throws Exception {
        if (gdxAcronymsTable == null) {
            Table table = oceanicAtmosphericAcronymsTable();
            StringArray acronymSA = (StringArray) (table.getColumn(0));
            StringArray fullNameSA = (StringArray) (table.getColumn(1));

            //remove some really common acronyms I don't want to expand
            BitSet keep = new BitSet();
            keep.set(0, acronymSA.size());
            String common[] = { //"DOC", "DOD", "DOE", "USDOC", "USDOD", "USDOE", 
                    "NOAA", "NASA", "US" };
            for (int c = 0; c < common.length; c++) {
                int po = acronymSA.indexOf(common[c]);
                if (po >= 0)
                    keep.clear(po);
            }
            table.justKeep(keep);

            gdxAcronymsTable = table; //swap into place
        }
        return gdxAcronymsTable;
    }

    /**
     * This returns the Oceanic/Atmospheric Acronyms table as a HashMap:
     * key=acronym, value=fullName.
     * THIS IS ONLY FOR GenerateDatasetsXml THREADS -- a few common acronyms are removed.
     *
     * @return the oceanic/atmospheric variable names table as a HashMap
     * @throws Exception if trouble (e.g., file not found)
     */
    public static HashMap<String, String> gdxAcronymsHashMap() throws Exception {
        if (gdxAcronymsHashMap == null) {
            Table table = gdxAcronymsTable();
            StringArray acronymSA = (StringArray) (table.getColumn(0));
            StringArray fullNameSA = (StringArray) (table.getColumn(1));
            int n = table.nRows();
            HashMap<String, String> hm = new HashMap();
            for (int i = 1; i < n; i++)
                hm.put(acronymSA.get(i), fullNameSA.get(i));
            gdxAcronymsHashMap = hm; //swap into place
        }
        return gdxAcronymsHashMap;
    }

    /**
     * This returns the Oceanic/Atmospheric Variable Names table as a HashMap:
     * key=variableName, value=fullName.
     * THIS IS ONLY FOR GenerateDatasetsXml THREADS.
     *
     * @return the oceanic/atmospheric variable names table as a HashMap
     * @throws Exception if trouble (e.g., file not found)
     */
    public static HashMap<String, String> gdxVariableNamesHashMap() throws Exception {
        if (gdxVariableNamesHashMap == null) {
            Table table = oceanicAtmosphericVariableNamesTable();
            StringArray varNameSA = (StringArray) (table.getColumn(0));
            StringArray fullNameSA = (StringArray) (table.getColumn(1));
            int n = table.nRows();
            HashMap<String, String> hm = new HashMap();
            for (int i = 1; i < n; i++)
                hm.put(varNameSA.get(i), fullNameSA.get(i));
            gdxVariableNamesHashMap = hm; //swap into place
        }
        return gdxVariableNamesHashMap;
    }

    /**
     * This returns the FIPS county table: col 0=FIPS (5-digit-FIPS), 1=Name (ST, County Name).
     * <br>States are included (their last 3 digits are 000).
     * <br>The table is sorted (case insensitive) by the county column.
     * <br>The most official source is http://www.itl.nist.gov/fipspubs/fip6-4.htm
     *
     * <p>The table is modified from http://www.census.gov/datamap/fipslist/AllSt.txt .
     * It includes the Appendix A and B counties from 
     * U.S. protectorates and county-equivalent entities of the freely associated atates
     * from http://www.itl.nist.gov/fipspubs/co-codes/states.txt .
     * I changed "lsabela" PR to "Isabela".
     *
     * @return the FIPS county table
     * @throws Exception if trouble (e.g., file not found)
     */
    public static Table fipsCountyTable() throws Exception {
        Table table = new Table();
        table.readASCII(contextDirectory + "WEB-INF/classes/gov/noaa/pfel/erddap/util/FipsCounty.tsv", 0, 1, "",
                null, null, null, null, false); //false = don't simplify
        return table;
    }

    /**
     * This returns the complete list of CF Standard Names as a table with 1 column.
     *
     * @return the complete list of CF Standard Names as a table with 1 column
     * @throws Exception if trouble (e.g., file not found)
     */
    public static Table keywordsCfTable() throws Exception {
        StringArray sa = StringArray
                .fromFile(contextDirectory + "WEB-INF/classes/gov/noaa/pfel/erddap/util/cfStdNames.txt");
        Table table = new Table();
        table.addColumn("CfStandardNames", sa);
        return table;
    }

    /**
     * This returns the complete list of GCMD Science Keywords as a table with 1 column.
     *
     * @return the complete list of GCMD Science Keywords as a table with 1 column
     * @throws Exception if trouble (e.g., file not found)
     */
    public static Table keywordsGcmdTable() throws Exception {
        StringArray sa = StringArray
                .fromFile(contextDirectory + "WEB-INF/classes/gov/noaa/pfel/erddap/util/gcmdScienceKeywords.txt");
        Table table = new Table();
        table.addColumn("GcmdScienceKeywords", sa);
        return table;
    }

    /**
     * This returns the complete CF to GCMD conversion information as a table with 1 column.
     * The GCMD to CF conversion information can be derived from this.
     *
     * @return the CF to GCMD conversion information as a table with 1 column
     * @throws Exception if trouble (e.g., file not found)
     */
    public static Table keywordsCfToGcmdTable() throws Exception {
        StringArray sa = StringArray
                .fromFile(contextDirectory + "WEB-INF/classes/gov/noaa/pfel/erddap/util/CfToGcmd.txt");
        Table table = new Table();
        table.addColumn("CfToGcmd", sa);
        return table;
    }

    /**
     * This returns true during the initial loadDatasets.
     * @return true during the initial loadDatasets, else false.
     */
    public static boolean initialLoadDatasets() {
        return majorLoadDatasetsTimeSeriesSB.length() == 0;
    }

    /** This is called by the ERDDAP constructor to initialize Lucene. */
    public static void initializeLucene() {
        //ERDDAP consciously doesn't use any stopWords (words not included in the index)
        //1) this matches the behaviour of the original searchEngine
        //2) this is what users expect, e.g., when searching for a phrase
        //3) the content here isn't prose, so the stop words aren't nearly as common
        HashSet stopWords = new HashSet();
        luceneAnalyzer = new StandardAnalyzer(luceneVersion, stopWords);
        //it is important that the queryParser use the same analyzer as the indexWriter
        luceneQueryParser = new QueryParser(luceneVersion, luceneDefaultField, luceneAnalyzer);
    }

    /** 
     * This creates an IndexWriter.
     * Normally, this is created once in RunLoadDatasets.
     * But if trouble, a new one will be created.
     *
     * @throws RuntimeException if trouble
     */
    public static void createLuceneIndexWriter(boolean firstTime) {

        try {
            String2.log("createLuceneIndexWriter(" + firstTime + ")");
            long tTime = System.currentTimeMillis();

            //if this is being called, directory shouldn't be locked
            //see javaDocs for indexWriter.close()
            if (IndexWriter.isLocked(luceneDirectory))
                IndexWriter.unlock(luceneDirectory);

            //create indexWriter
            IndexWriterConfig lucConfig = new IndexWriterConfig(luceneVersion, luceneAnalyzer);
            lucConfig.setOpenMode(
                    firstTime ? IndexWriterConfig.OpenMode.CREATE : IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
            luceneIndexWriter = new IndexWriter(luceneDirectory, lucConfig);
            luceneIndexWriter.setInfoStream(verbose ? new PrintStream(new String2LogOutputStream()) : null);
            String2.log("  createLuceneIndexWriter finished.  time=" + (System.currentTimeMillis() - tTime) + "ms");
        } catch (Throwable t) {
            throw new RuntimeException(t);
        }
    }

    /** 
     * This returns the Lucene IndexSearcher and datasetIDFieldCache (thread-safe).
     * IndexSearch uses IndexReader (also thread-safe).
     * IndexReader works on a snapshot of an index, 
     * so it is recreated if flagged at end of LoadDatasetsevery 
     * via needNewLuceneIndexReader.
     *
     * @return Object[2]: [0]=a luceneIndexSearcher (thread-safe), or null if trouble.
     *    [1]=luceneDatasetIDFieldCache, or null if trouble.
     *    Either both will be null or both will be !null.
     */
    public static Object[] luceneIndexSearcher() {

        //synchronize 
        synchronized (luceneIndexReaderLock) {

            //need a new indexReader?
            //(indexReader is thread-safe, but only need one)
            if (luceneIndexReader == null || needNewLuceneIndexReader || luceneIndexSearcher == null) {

                //clear out old one
                try {
                    if (luceneIndexSearcher != null)
                        luceneIndexSearcher.close();
                } catch (Throwable t) {
                }
                luceneIndexSearcher = null;

                try {
                    if (luceneIndexReader != null)
                        luceneIndexReader.close();
                } catch (Throwable t) {
                }
                luceneIndexReader = null;
                luceneDatasetIDFieldCache = null;

                needNewLuceneIndexReader = true;

                //create a new one
                try {
                    long rTime = System.currentTimeMillis();
                    luceneIndexReader = IndexReader.open(luceneDirectory); // read-only=true
                    luceneIndexSearcher = new IndexSearcher(luceneIndexReader);
                    String2.log(
                            "  new luceneIndexReader+Searcher time=" + (System.currentTimeMillis() - rTime) + "ms");

                    //create the luceneDatasetIDFieldCache
                    //save memory by sharing the canonical strings  
                    //(EDD.ensureValid makes datasetID's canonical)
                    rTime = System.currentTimeMillis();
                    luceneDatasetIDFieldCache = FieldCache.DEFAULT.getStrings(luceneIndexReader, "datasetID");
                    int n = luceneDatasetIDFieldCache.length;
                    for (int i = 0; i < n; i++)
                        luceneDatasetIDFieldCache[i] = String2.canonical(luceneDatasetIDFieldCache[i]);
                    String2.log(
                            "  new luceneDatasetIDFieldCache time=" + (System.currentTimeMillis() - rTime) + "ms");

                    //if successful, we no longer needNewLuceneIndexReader
                    needNewLuceneIndexReader = false;
                    //if successful, fall through

                } catch (Throwable t) {
                    String subject = String2.ERROR + " while creating Lucene Searcher";
                    String msg = MustBe.throwableToString(t);
                    email(emailEverythingToCsv, subject, msg);
                    String2.log(subject + "\n" + msg);

                    //clear out old one
                    try {
                        if (luceneIndexSearcher != null)
                            luceneIndexSearcher.close();
                    } catch (Throwable t2) {
                    }
                    luceneIndexSearcher = null;

                    try {
                        if (luceneIndexReader != null)
                            luceneIndexReader.close();
                    } catch (Throwable t2) {
                    }
                    luceneIndexReader = null;
                    luceneDatasetIDFieldCache = null;

                    needNewLuceneIndexReader = true;

                    //return
                    return new Object[] { null, null };
                }
            }

            return new Object[] { luceneIndexSearcher, luceneDatasetIDFieldCache };
        }
    }

    /** 
     * This parses a query with luceneQueryParser (not thread-safe).
     *
     * @param searchString  the user's searchString, but modified slightly for Lucene
     * @return a Query, or null if trouble
     */
    public static Query luceneParseQuery(String searchString) {

        //queryParser is not thread-safe, so re-use it in a synchronized block
        //(It is fast, so synchronizing on one parser shouldn't be a bottleneck.
        synchronized (luceneQueryParser) {

            try {
                //long qTime = System.currentTimeMillis();
                Query q = luceneQueryParser.parse(searchString);
                //String2.log("  luceneParseQuery finished.  time=" + (System.currentTimeMillis() - qTime) + "ms"); //always 0
                return q;
            } catch (Throwable t) {
                String2.log(
                        "Lucene failed to parse searchString=" + searchString + "\n" + MustBe.throwableToString(t));
                return null;
            }
        }
    }

    /**
     * This gets the raw requested (or inferred) page number and itemsPerPage
     * by checking the request parameters.
     *
     * @param request
     * @return int[2]  
     *     [0]=page         (may be invalid, e.g., -5 or Integer.MAX_VALUE) 
     *     [1]=itemsPerPage (may be invalid, e.g., -5 or Integer.MAX_VALUE)
     */
    public static int[] getRawRequestedPIpp(HttpServletRequest request) {

        return new int[] { String2.parseInt(request.getParameter("page")),
                String2.parseInt(request.getParameter("itemsPerPage")) };
    }

    /**
     * This gets the requested (or inferred) page number and itemsPerPage
     * by checking the request parameters.
     *
     * @param request
     * @return int[2]  
     *     [0]=page (will be 1..., but may be too big), 
     *     [1]=itemsPerPage (will be 1...), 
     */
    public static int[] getRequestedPIpp(HttpServletRequest request) {

        int iar[] = getRawRequestedPIpp(request);

        //page is 1..
        if (iar[0] < 1 || iar[0] == Integer.MAX_VALUE)
            iar[0] = 1; //default

        //itemsPerPage
        if (iar[1] < 1 || iar[1] == Integer.MAX_VALUE)
            iar[1] = defaultItemsPerPage;

        return iar;
    }

    /**
     * This returns the .jsonp=[functionName] part of the request (percent encoded) or "".
     * If not "", it will have "&" at the end.
     *
     * @param request
     * @return the .jsonp=[functionName] part of the request (percent encoded) or "".
     *   If not "", it will have "&" at the end.
     *   If the query has a syntax error, this returns "".
     *   If the !String2.isVariableNameSafe(functionName), this throws a SimpleException.
     */
    public static String passThroughJsonpQuery(HttpServletRequest request) {
        String jsonp = "";
        try {
            String parts[] = Table.getDapQueryParts(request.getQueryString()); //decoded.  Does some validity checking.
            jsonp = String2.stringStartsWith(parts, ".jsonp="); //may be null
            if (jsonp == null)
                return "";
            String functionName = jsonp.substring(7); //it will be because it starts with .jsonp=
            if (!String2.isJsonpNameSafe(functionName))
                throw new SimpleException(errorJsonpFunctionName);
            return ".jsonp=" + SSR.minimalPercentEncode(functionName) + "&";
        } catch (Throwable t) {
            String2.log(MustBe.throwableToString(t));
            return "";
        }
    }

    /**
     * This extracts the page= and itemsPerPage= parameters
     * of the request (if any) and returns them lightly validated and formatted for a URL
     * (e.g., "page=1&amp;itemsPerPage=1000").
     *
     * @param request
     * @return e.g., "page=1&amp;itemsPerPage=1000" (encoded here for JavaDocs)
     */
    public static String passThroughPIppQuery(HttpServletRequest request) {
        int pipp[] = getRequestedPIpp(request);
        return "page=" + pipp[0] + "&itemsPerPage=" + pipp[1];
    }

    /**
     * This is like passThroughPIppQuery, but always sets page=1.
     *
     * @param request
     * @return e.g., "page=1&amp;itemsPerPage=1000" (encoded here for JavaDocs)
     */
    public static String passThroughPIppQueryPage1(HttpServletRequest request) {
        int pipp[] = getRequestedPIpp(request);
        return "page=1&itemsPerPage=" + pipp[1];
    }

    /**
     * This is like passThroughPIppQuery, but the ampersand is XML encoded so
     * it is ready to be put into HTML.
     *
     * @param request
     * @return e.g., "page=1&amp;amp;itemsPerPage=1000" (doubly encoded here for JavaDocs)
     */
    public static String encodedPassThroughPIppQuery(HttpServletRequest request) {
        int pipp[] = getRequestedPIpp(request);
        return "page=" + pipp[0] + "&amp;itemsPerPage=" + pipp[1];
    }

    /**
     * This is like encodedPassThroughPIppQuery, but always sets page=1.
     *
     * @param request
     * @return e.g., "page=1&amp;amp;itemsPerPage=1000" (doubly encoded here for JavaDocs)
     */
    public static String encodedPassThroughPIppQueryPage1(HttpServletRequest request) {
        int pipp[] = getRequestedPIpp(request);
        return "page=1&amp;itemsPerPage=" + pipp[1];
    }

    /**
     * This calculates the requested (or inferred) page number and itemsPerPage
     * by checking the request parameters.
     *
     * @param request
     * @param nItems e.g., total number of datasets from search results
     * @return int[4]  
     *     [0]=page (will be 1..., but may be too big), 
     *     [1]=itemsPerPage (will be 1...), 
     *     [2]=startIndex (will be 0..., but may be too big),
     *     [3]=lastPage with items (will be 1...).
     *     Note that page may be greater than lastPage (caller should write error message to user).
     */
    public static int[] calculatePIpp(HttpServletRequest request, int nItems) {

        int pipp[] = getRequestedPIpp(request);
        int page = pipp[0];
        int itemsPerPage = pipp[1];
        int startIndex = Math2.narrowToInt((page - 1) * (long) itemsPerPage); //0..
        int lastPage = Math.max(1, Math2.hiDiv(nItems, itemsPerPage));

        return new int[] { page, itemsPerPage, startIndex, lastPage };
    }

    /** This returns the error String[2] if a search yielded no matches.
     *
     */
    public static String[] noSearchMatch(String searchFor) {
        if (searchFor == null)
            searchFor = "";
        return new String[] { MustBe.THERE_IS_NO_DATA, (searchFor.length() > 0 ? searchSpelling + " " : "")
                + (searchFor.indexOf(' ') >= 0 ? searchFewerWords : "") };
    }

    /** 
     * This returns the error String[2] if page &gt; lastPage.
     *
     * @param page
     * @param lastPage
     * @return String[2] with the two Strings
     */
    public static String[] noPage(int page, int lastPage) {
        return new String[] { MessageFormat.format(noPage1, "" + page, "" + lastPage),
                MessageFormat.format(noPage2, "" + page, "" + lastPage) };
    }

    /**
     * This returns the nMatchingDatasets HTML message, with paging options.
     *
     * @param nMatches   this must be 1 or more
     * @param page
     * @param lastPage
     * @param relevant true=most relevant first, false=sorted alphabetically
     * @param urlWithQuery percentEncoded, but not HTML/XML encoded, 
     *    e.g., http://coastwatch.pfeg.noaa.gov/erddap/search/index.html?page=1&itemsPerPage=1000&searchFor=temperature%20wind
     * @return string with HTML content
     */
    public static String nMatchingDatasetsHtml(int nMatches, int page, int lastPage, boolean relevant,
            String urlWithQuery) {

        if (nMatches == 1)
            return nMatching1;

        StringBuilder results = new StringBuilder(
                MessageFormat.format(relevant ? nMatchingMostRelevant : nMatchingAlphabetical, "" + nMatches)
                        + "\n");

        if (lastPage > 1) {

            //figure out where page number is so replaceable
            int pagePo = urlWithQuery.indexOf("?page=");
            if (pagePo < 0)
                pagePo = urlWithQuery.indexOf("&page=");
            if (pagePo < 0) {
                pagePo = urlWithQuery.length();
                urlWithQuery += (urlWithQuery.indexOf('?') < 0 ? "?" : "&") + "page=" + page;
            }
            int pageNumberPo = pagePo + 6;

            int ampPo = urlWithQuery.indexOf('&', pageNumberPo);
            if (ampPo < 0)
                ampPo = urlWithQuery.length();

            String url1 = "&nbsp;<a href=\"" + XML.encodeAsHTMLAttribute(urlWithQuery.substring(0, pageNumberPo)); // + p
            String url2 = XML.encodeAsHTMLAttribute(urlWithQuery.substring(ampPo)) + "\">"; // + p   
            String url3 = "</a>&nbsp;\n";

            //links, e.g. if page=5 and lastPage=12: _1 ... _4  5 _6 ... _12 
            StringBuilder sb = new StringBuilder();
            if (page >= 2)
                sb.append(url1 + 1 + url2 + 1 + url3);
            if (page >= 4)
                sb.append("...\n");
            if (page >= 3)
                sb.append(url1 + (page - 1) + url2 + (page - 1) + url3);
            sb.append("&nbsp;" + page + "&nbsp;(" + EDStatic.nMatchingCurrent + ")&nbsp;\n"); //always show current page
            if (page <= lastPage - 2)
                sb.append(url1 + (page + 1) + url2 + (page + 1) + url3);
            if (page <= lastPage - 3)
                sb.append("...\n");
            if (page <= lastPage - 1)
                sb.append(url1 + lastPage + url2 + lastPage + url3);

            //append to results
            results.append("&nbsp;&nbsp;"
                    + MessageFormat.format(nMatchingPage, "" + page, "" + lastPage, sb.toString()) + "\n");
        }

        return results.toString();
    }

    /** If query is null or "", this returns ""; otherwise, this returns "?" + query. */
    public static String questionQuery(String query) {
        return query == null || query.length() == 0 ? "" : "?" + query;
    }

    /** 
     * This updates out-of-date http: references to https: within a string.
     * This is very safe won't otherwise change the string (even "" or null).
     */
    public static String updateUrls(String s) {
        if (!String2.isSomething(s))
            return s;

        //change some non-http things
        StringBuilder sb = new StringBuilder(s);
        String2.replaceAll(sb, //reversed in naming_authority
                "gov.noaa.pfel.", "gov.noaa.pfeg.");

        if (sb.indexOf("http") < 0)
            return sb.toString();

        int n = updateUrlsFrom.length;
        for (int i = 0; i < n; i++)
            String2.replaceAll(sb, updateUrlsFrom[i], updateUrlsTo[i]);
        return sb.toString();
    }

    /**
     * This calls updateUrls for every String attribute (except EDStatic.updateUrlsSkipAttributes)
     * and writes changes to addAtts.
     * 
     * @param sourceAtts may be null
     * @param addAtts mustn't be null.
     */
    public static void updateUrls(Attributes sourceAtts, Attributes addAtts) {
        //get all the attribute names
        HashSet<String> hs = new HashSet();
        String names[];
        if (sourceAtts != null) {
            names = sourceAtts.getNames();
            for (int i = 0; i < names.length; i++)
                hs.add(names[i]);
        }
        names = addAtts.getNames();
        for (int i = 0; i < names.length; i++)
            hs.add(names[i]);
        names = hs.toArray(new String[] {});

        //updateUrls in all attributes
        for (int i = 0; i < names.length; i++) {
            if (String2.indexOf(updateUrlsSkipAttributes, names[i]) >= 0)
                continue;
            PrimitiveArray pa = addAtts.get(names[i]);
            if (pa == null && sourceAtts != null)
                pa = sourceAtts.get(names[i]);
            if (pa != null && pa.size() > 0 && pa.elementClass() == String.class) {
                String oValue = pa.getString(0);
                String value = updateUrls(oValue);
                if (!value.equals(oValue))
                    addAtts.set(names[i], value);
            }
        }
    }

    /**
     * This indicates if t is a ClientAbortException.
     *
     * @param t the exception
     * @return true if t is a ClientAbortException.
     * @throws Throwable
     */
    public static boolean isClientAbortException(Throwable t) {
        String tString = t.toString();
        return tString.indexOf("ClientAbortException") >= 0;
    }

    /**
     * If t is a ClientAbortException, this will rethrow it.
     * org.apache.catalina.connector.ClientAbortException is hard to catch
     * since catalina code is linked in after deployment.
     * So this looks for the string.
     *
     * Normal use: Use this first thing in catch, before throwing WaitThenTryAgainException.
     *
     * @param t the exception which will be thrown again if it is a ClientAbortException
     * @throws Throwable
     */
    public static void rethrowClientAbortException(Throwable t) throws Throwable {
        if (isClientAbortException(t))
            throw t;
    }

    public static void testUpdateUrls() throws Exception {
        String2.log("\n***** EDStatic.testUpdateUrls");
        Attributes source = new Attributes();
        Attributes add = new Attributes();
        source.set("a", "http://coastwatch.pfel.noaa.gov");
        source.set("nine", 9.0);
        add.set("b", "http://www.whoi.edu");
        add.set("ten", 10.0);
        add.set("sourceUrl", "http://coastwatch.pfel.noaa.gov");
        EDStatic.updateUrls(source, add);
        String results = add.toString();
        String expected = "    a=https://coastwatch.pfeg.noaa.gov\n" + "    b=https://www.whoi.edu\n"
                + "    sourceUrl=http://coastwatch.pfel.noaa.gov\n" + //unchanged
                "    ten=10.0d\n";
        Test.ensureEqual(results, expected, "results=\n" + results);

        source = new Attributes();
        add = new Attributes();
        add.set("a", "http://coastwatch.pfel.noaa.gov");
        add.set("b", "http://www.whoi.edu");
        add.set("nine", 9.0);
        add.set("sourceUrl", "http://coastwatch.pfel.noaa.gov");
        EDStatic.updateUrls(null, add);
        results = add.toString();
        expected = "    a=https://coastwatch.pfeg.noaa.gov\n" + "    b=https://www.whoi.edu\n" + "    nine=9.0d\n"
                + "    sourceUrl=http://coastwatch.pfel.noaa.gov\n"; //unchanged
        Test.ensureEqual(results, expected, "results=\n" + results);
    }

    /** 
     * This tests some of the methods in this class.
     * @throws an Exception if trouble.
     */
    public static void test() throws Throwable {
        String2.log("\n*** EDStatic.test");
        testUpdateUrls();
    }

}