001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028package edu.wisc.ssec.mcidasv.servermanager;
029
030import static java.util.Objects.requireNonNull;
031
032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
033import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashMap;
034import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
035
036import java.io.File;
037import java.io.IOException;
038
039import java.nio.file.Path;
040import java.nio.file.Paths;
041import java.util.Collection;
042import java.util.Collections;
043import java.util.EnumSet;
044import java.util.LinkedHashSet;
045import java.util.List;
046import java.util.Map;
047import java.util.Set;
048import java.util.stream.Collectors;
049
050import org.apache.commons.collections4.trie.PatriciaTrie;
051import org.bushe.swing.event.EventBus;
052import org.bushe.swing.event.annotation.AnnotationProcessor;
053import org.bushe.swing.event.annotation.EventSubscriber;
054
055import org.python.modules.posix.PosixModule;
056
057import org.slf4j.Logger;
058import org.slf4j.LoggerFactory;
059
060import org.w3c.dom.Element;
061
062import ucar.unidata.util.LogUtil;
063import ucar.unidata.idv.IdvObjectStore;
064import ucar.unidata.idv.IdvResourceManager;
065import ucar.unidata.idv.chooser.adde.AddeServer;
066import ucar.unidata.xml.XmlResourceCollection;
067
068import edu.wisc.ssec.mcidasv.Constants;
069import edu.wisc.ssec.mcidasv.McIDASV;
070import edu.wisc.ssec.mcidasv.ResourceManager;
071import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
072import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
073import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
074import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
075import edu.wisc.ssec.mcidasv.servermanager.AddeThread.McservEvent;
076
077/**
078 * McIDAS-V ADDE server manager. This class is essentially the
079 * {@literal "gatekeeper"} for anything having to do with the application's
080 * collection of ADDE servers. This class is also responsible for controlling
081 * the thread used to manage the external mcservl binary.
082 *
083 * @see AddeThread
084 */
085public class EntryStore {
086
087    /** 
088     * Property that allows users to supply arbitrary paths to McIDAS-X 
089     * binaries used by mcservl.
090     * 
091     * @see #getAddeRootDirectory()
092     */
093    private static final String PROP_DEBUG_LOCALROOT =
094        "debug.localadde.rootdir";
095
096    /**
097     * Property that allows users to control debug output from ADDE requests.
098     * 
099     * @see #isAddeDebugEnabled(boolean)
100     * @see #setAddeDebugEnabled(boolean)
101     */
102    private static final String PROP_DEBUG_ADDEURL = "debug.adde.reqs";
103
104    /**
105     * {@literal "Userpath"} not writable error message.
106     * This is the one that is shown in the GUI via
107     * {@link LogUtil#userErrorMessage(String)}.
108     */
109    private static final String ERROR_LOGUTIL_USERPATH =
110        "Local servers cannot write to userpath:\n%s";
111
112    /**
113     * {@literal "Userpath"} not writable error message.
114     * This one is used by the logging system.
115     */
116    private static final String ERROR_USERPATH =
117        "Local servers cannot write to userpath ('{}')";
118
119    /**
120     * SLF4J-style formatting string for use when {@code RESOLV.SRV} can not
121     * be found. .
122     */
123    private static final String WARN_NO_RESOLVSRV =
124        "EntryStore: RESOLV.SRV missing; expected='{}'";
125
126    /** Enumeration of the various server manager events. */
127    public enum Event { 
128        /** Entries were replaced. */
129        REPLACEMENT, 
130        /** Entries were removed. */
131        REMOVAL, 
132        /** Entries were added. */
133        ADDITION, 
134        /** Entries were updated.*/
135        UPDATE, 
136        /** Something failed! */
137        FAILURE, 
138        /** Local servers started. */
139        STARTED, 
140        /** Catch-all? */
141        UNKNOWN 
142    }
143
144    /** Logging object. */
145    private static final Logger logger =
146        LoggerFactory.getLogger(EntryStore.class);
147
148    /** Preference key for ADDE entries. */
149    private static final String PREF_ADDE_ENTRIES = "mcv.servers.entries";
150
151    /** The ADDE servers known to McIDAS-V. */
152    private final PatriciaTrie<AddeEntry> trie;
153
154    /** {@literal "Root"} local server directory. */
155    private final String ADDE_DIRECTORY;
156
157    /** Path to local server binaries. */
158    private final String ADDE_BIN;
159
160    /** Path to local server data. */
161    private final String ADDE_DATA;
162
163    /** Path to mcservl. */
164    private final String ADDE_MCSERVL;
165
166    /** Path to the user's {@literal "userpath"} directory. */
167    private final String USER_DIRECTORY;
168
169    /** Path to the user's {@literal "RESOLV.SRV"}. */
170    private final String ADDE_RESOLV;
171
172    /** Which port is this particular manager operating on */
173    private static String localPort;
174
175    /** Thread that monitors the mcservl process. */
176    private static AddeThread thread;
177
178    /** Last {@link AddeEntry AddeEntries} added to the manager. */
179    private final List<AddeEntry> lastAdded;
180
181    /** McIDAS-V preferences store. */
182    private final IdvObjectStore idvStore;
183
184    /**
185     * Constructs a server manager.
186     * 
187     * @param store McIDAS-V's preferences store. Cannot be {@code null}.
188     * @param rscManager McIDAS-V's resource manager. Cannot be {@code null}.
189     *
190     * @throws NullPointerException if either of {@code store} or
191     * {@code rscManager} is {@code null}.
192     */
193    public EntryStore(final IdvObjectStore store,
194                      final IdvResourceManager rscManager)
195    {
196        requireNonNull(store);
197        requireNonNull(rscManager);
198
199        this.idvStore = store;
200        this.trie = new PatriciaTrie<>();
201        this.ADDE_DIRECTORY = getAddeRootDirectory();
202        this.ADDE_BIN = ADDE_DIRECTORY + File.separator + "bin";
203        this.ADDE_DATA = ADDE_DIRECTORY + File.separator + "data";
204        EntryStore.localPort = Constants.LOCAL_ADDE_PORT;
205        this.lastAdded = arrList();
206        AnnotationProcessor.process(this);
207
208        McIDASV mcv = McIDASV.getStaticMcv();
209        USER_DIRECTORY = mcv.getUserDirectory();
210        ADDE_RESOLV = mcv.getUserFile("RESOLV.SRV");
211
212        if (McIDASV.isWindows()) {
213            ADDE_MCSERVL = ADDE_BIN + "\\mcservl.exe";
214        } else {
215            ADDE_MCSERVL = ADDE_BIN + "/mcservl";
216        }
217
218        try {
219            Set<LocalAddeEntry> locals =
220                EntryTransforms.readResolvFile(ADDE_RESOLV);
221            putEntries(trie, locals);
222        } catch (IOException e) {
223            logger.warn(WARN_NO_RESOLVSRV, ADDE_RESOLV);
224        }
225
226        XmlResourceCollection userResource =
227            rscManager.getXmlResources(ResourceManager.RSC_NEW_USERSERVERS);
228        XmlResourceCollection sysResource =
229            rscManager.getXmlResources(IdvResourceManager.RSC_ADDESERVER);
230
231        Set<AddeEntry> systemEntries =
232            extractResourceEntries(EntrySource.SYSTEM, sysResource);
233
234        Set<AddeEntry> prefEntries = extractPreferencesEntries(store);
235        prefEntries = removeDeletedSystemEntries(prefEntries, systemEntries);
236
237        Set<AddeEntry> userEntries = extractUserEntries(userResource);
238        userEntries = removeDeletedSystemEntries(userEntries, systemEntries);
239
240        putEntries(trie, systemEntries);
241        putEntries(trie, userEntries);
242        putEntries(trie, prefEntries);
243
244        saveEntries();
245    }
246
247    /**
248     * Searches {@code entries} for {@link AddeEntry} objects with two characteristics:
249     * <ul>
250     * <li>the object source is {@link EntrySource#SYSTEM}</li>
251     * <li>the object is <b>not</b> in {@code systemEntries}</li>
252     * </ul>
253     * 
254     * <p>The intent behind this method is to safely remove {@literal "system"}
255     * entries that have been stored to a user's preferences. {@code entries}
256     * can be generated from anywhere you like, but {@code systemEntries} should
257     * almost always be created from {@literal "addeservers.xml"}.
258     * 
259     * @param entries Cannot be {@code null}.
260     * @param systemEntries Cannot be {@code null}.
261     * 
262     * @return {@code Set} of entries that are not system resources that have
263     * been removed, or an empty {@code Set}.
264     */
265    private static Set<AddeEntry> removeDeletedSystemEntries(
266        final Collection<? extends AddeEntry> entries,
267        final Collection<? extends AddeEntry> systemEntries)
268    {
269        Set<AddeEntry> pruned = newLinkedHashSet(entries.size());
270        for (AddeEntry entry : entries) {
271            if (entry.getEntrySource() != EntrySource.SYSTEM) {
272                pruned.add(entry);
273            } else if (systemEntries.contains(entry)) {
274                pruned.add(entry);
275            }
276        }
277        return pruned;
278    }
279
280    /**
281     * Adds {@link AddeEntry} objects to a given {@link PatriciaTrie}.
282     * 
283     * @param trie Cannot be {@code null}.
284     * @param newEntries Cannot be {@code null}.
285     */
286    private static void putEntries(
287        final PatriciaTrie<AddeEntry> trie,
288        final Collection<? extends AddeEntry> newEntries)
289    {
290        requireNonNull(trie);
291        requireNonNull(newEntries);
292
293        for (AddeEntry e : newEntries) {
294            trie.put(e.asStringId(), e);
295        }
296    }
297
298    /**
299     * Returns the {@link IdvObjectStore} used to save user preferences.
300     *
301     * @return {@code IdvObjectStore} used by the rest of McIDAS-V.
302     */
303    public IdvObjectStore getIdvStore() {
304        return idvStore;
305    }
306
307    /**
308     * Returns environment variables that allow mcservl to run on Windows.
309     *
310     * @return {@code String} array containing mcservl's environment variables.
311     */
312    protected String[] getWindowsAddeEnv() {
313        // Drive letters should come from environment
314        // Java drive is not necessarily system drive
315        return new String[] {
316            "PATH=" + ADDE_BIN,
317            "MCPATH=" + USER_DIRECTORY+':'+ADDE_DATA,
318            "MCUSERDIR=" + USER_DIRECTORY,
319            "MCNOPREPEND=1",
320            "MCTRACE=" + (Boolean.parseBoolean(System.getProperty("debug.adde.reqs", "false")) ? "1" : "0"),
321            "MCTRACK=NO",
322            "MCJAVAPATH=" + System.getProperty("java.home"),
323            "MCBUFRJARPATH=" + ADDE_BIN,
324            "SYSTEMDRIVE=" + System.getenv("SystemDrive"),
325            "SYSTEMROOT=" + System.getenv("SystemRoot"),
326            "HOMEDRIVE=" + System.getenv("HOMEDRIVE"),
327            "HOMEPATH=\\Windows"
328        };
329    }
330
331    /**
332     * Returns environment variables that allow mcservl to run on
333     * {@literal "unix-like"} systems.
334     *
335     * @return {@code String} array containing mcservl's environment variables.
336     */
337    protected String[] getUnixAddeEnv() {
338        return new String[] {
339            "PATH=" + ADDE_BIN,
340            "HOME=" + System.getenv("HOME"),
341            "USER=" + System.getenv("USER"),
342            "MCPATH=" + USER_DIRECTORY+':'+ADDE_DATA,
343            "MCUSERDIR=" + USER_DIRECTORY,
344            "LD_LIBRARY_PATH=" + ADDE_BIN,
345            "DYLD_LIBRARY_PATH=" + ADDE_BIN,
346            "MCNOPREPEND=1",
347            "MCTRACE=" + (Boolean.parseBoolean(System.getProperty("debug.adde.reqs", "false")) ? "1" : "0"),
348            "MCTRACK=NO",
349            "MCJAVAPATH=" + System.getProperty("java.home"),
350            "MCBUFRJARPATH=" + ADDE_BIN
351        };
352    }
353
354    /**
355     * Returns command line used to launch mcservl.
356     *
357     * @return {@code String} array that represents an invocation of mcservl.
358     */
359    protected String[] getAddeCommands() {
360        String mcvPID = Integer.toString(PosixModule.getpid());
361        if (McIDASV.isWindows() || (mcvPID == null) || "0".equals(mcvPID)) {
362            return new String[] { ADDE_MCSERVL, "-v", "-p", localPort };
363        } else {
364            return new String[] {
365                ADDE_MCSERVL, "-v", "-p", localPort, "-i", mcvPID
366            };
367        }
368    }
369
370    /**
371     * Determine the validity of a given {@link AddeEntry}.
372     * 
373     * @param entry Entry to check. Cannot be {@code null}.
374     * 
375     * @return {@code true} if {@code entry} is invalid or {@code false}
376     * otherwise.
377     *
378     * @throws NullPointerException if {@code entry} is {@code null}.
379     * @throws AssertionError if {@code entry} is somehow neither a
380     * {@code RemoteAddeEntry} or {@code LocalAddeEntry}.
381     * 
382     * @see LocalAddeEntry#INVALID_ENTRY
383     * @see RemoteAddeEntry#INVALID_ENTRY
384     */
385    public static boolean isInvalidEntry(final AddeEntry entry) {
386        requireNonNull(entry);
387
388        boolean retVal = true;
389        if (entry instanceof RemoteAddeEntry) {
390            retVal = RemoteAddeEntry.INVALID_ENTRY.equals(entry);
391        } else if (entry instanceof LocalAddeEntry) {
392            retVal = LocalAddeEntry.INVALID_ENTRY.equals(entry);
393        } else {
394            String clsName = entry.getClass().getName();
395            throw new AssertionError("Unknown AddeEntry type: "+clsName);
396        }
397        return retVal;
398    }
399
400    /**
401     * Returns the {@link AddeEntry AddeEntries} stored in the user's
402     * preferences.
403     * 
404     * @param store Object store that represents the user's preferences.
405     * Cannot be {@code null}.
406     * 
407     * @return Either the {@code AddeEntrys} stored in the prefs or an empty
408     * {@link Set}.
409     */
410    private Set<AddeEntry> extractPreferencesEntries(
411        final IdvObjectStore store)
412    {
413        assert store != null;
414
415        // this is valid--the only thing ever written to 
416        // PREF_REMOTE_ADDE_ENTRIES is an ArrayList of RemoteAddeEntry objects.
417        @SuppressWarnings("unchecked")
418        List<AddeEntry> asList = 
419            (List<AddeEntry>)store.get(PREF_ADDE_ENTRIES);
420        Set<AddeEntry> entries;
421        if (asList == null) {
422            entries = Collections.emptySet();
423        } else {
424            entries = newLinkedHashSet(asList.size());
425            entries.addAll(
426                asList.stream()
427                      .filter(entry -> entry instanceof RemoteAddeEntry)
428                      .collect(Collectors.toList()));
429        }
430        return entries;
431    }
432
433    /**
434     * Responds to server manager events being passed with the event bus. 
435     * 
436     * @param evt Event to which this method is responding. Cannot be
437     * {@code null}.
438     *
439     * @throws NullPointerException if {@code evt} is {@code null}.
440     */
441    @EventSubscriber(eventClass=Event.class)
442    public void onEvent(Event evt) {
443        requireNonNull(evt);
444
445        saveEntries();
446    }
447
448    /**
449     * Saves the current set of ADDE servers to the user's preferences and
450     * {@link #ADDE_RESOLV}.
451     */
452    public void saveEntries() {
453        idvStore.put(PREF_ADDE_ENTRIES, arrList(trie.values()));
454        idvStore.saveIfNeeded();
455        try {
456            EntryTransforms.writeResolvFile(ADDE_RESOLV, getLocalEntries());
457        } catch (IOException e) {
458            logger.error(WARN_NO_RESOLVSRV, ADDE_RESOLV);
459        }
460    }
461
462    /**
463     * Saves the list of ADDE entries to both the user's preferences and
464     * {@link #ADDE_RESOLV}.
465     */
466    public void saveForShutdown() {
467        idvStore.put(PREF_ADDE_ENTRIES, arrList(getPersistedEntrySet()));
468        idvStore.saveIfNeeded();
469        try {
470            EntryTransforms.writeResolvFile(ADDE_RESOLV,
471                getPersistedLocalEntries());
472        } catch (IOException e) {
473            logger.error(WARN_NO_RESOLVSRV, ADDE_RESOLV);
474        }
475    }
476
477    /**
478     * Searches the newest entries for the entries of the given
479     * {@link EntryType}.
480     * 
481     * @param type Look for entries matching this {@code EntryType}.
482     * Cannot be {@code null}.
483     * 
484     * @return Either a {@link List} of entries or an empty {@code List}.
485     *
486     * @throws NullPointerException if {@code type} is {@code null}.
487     */
488    public List<AddeEntry> getLastAddedByType(final EntryType type) {
489        requireNonNull(type);
490
491        List<AddeEntry> entries = arrList(lastAdded.size());
492        entries.addAll(lastAdded.stream()
493                                .filter(entry -> entry.getEntryType() == type)
494                                .collect(Collectors.toList()));
495        return entries;
496    }
497
498    /**
499     * Returns the {@link AddeEntry AddeEntries} that were added last, filtered
500     * by the given {@link EntryType EntryTypes}.
501     *
502     * @param types Filter the last added entries by these entry type.
503     * Cannot be {@code null}.
504     *
505     * @return {@link List} of the last added entries, filtered by
506     * {@code types}.
507     *
508     * @throws NullPointerException if {@code types} is {@code null}.
509     */
510    public List<AddeEntry> getLastAddedByTypes(final EnumSet<EntryType> types) {
511        requireNonNull(types);
512
513        List<AddeEntry> entries = arrList(lastAdded.size());
514        entries.addAll(
515            lastAdded.stream()
516                .filter(entry -> types.contains(entry.getEntryType()))
517                .collect(Collectors.toList()));
518        return entries;
519    }
520
521    /**
522     * Returns the {@link AddeEntry AddeEntries} that were added last. Note
523     * that this value is <b>not</b> preserved between sessions.
524     *
525     * @return {@link List} of the last ADDE entries that were added. May be
526     * empty.
527     */
528    public List<AddeEntry> getLastAdded() {
529        return arrList(lastAdded);
530    }
531
532    /**
533     * Returns the {@link Set} of {@link AddeEntry AddeEntries} that are known
534     * to work (for a given {@link EntryType} of entries).
535     * 
536     * @param type The {@code EntryType} you are interested in. Cannot be
537     * {@code null}.
538     * 
539     * @return A {@code Set} of matching remote ADDE entries. If there were no
540     * matches, an empty {@code Set} is returned.
541     *
542     * @throws NullPointerException if {@code type} is {@code null}.
543     */
544    public Set<AddeEntry> getVerifiedEntries(final EntryType type) {
545        requireNonNull(type);
546
547        Set<AddeEntry> verified = newLinkedHashSet(trie.size());
548        for (AddeEntry entry : trie.values()) {
549            if (entry.getEntryType() != type) {
550                continue;
551            }
552
553            if (entry instanceof LocalAddeEntry) {
554                verified.add(entry);
555            } else if (entry.getEntryValidity() == EntryValidity.VERIFIED) {
556                verified.add(entry);
557            }
558        }
559        return verified;
560    }
561
562    /**
563     * Returns the available {@link AddeEntry AddeEntries}, grouped by
564     * {@link EntryType}.
565     *
566     * @return {@link Map} of {@code EntryType} to a {@link Set} containing all
567     * of the entries that match that {@code EntryType}.
568     */
569    public Map<EntryType, Set<AddeEntry>> getVerifiedEntriesByTypes() {
570        Map<EntryType, Set<AddeEntry>> entryMap =
571            newLinkedHashMap(EntryType.values().length);
572
573        int size = trie.size();
574
575        for (EntryType type : EntryType.values()) {
576            entryMap.put(type, new LinkedHashSet<>(size));
577        }
578
579        for (AddeEntry entry : trie.values()) {
580            Set<AddeEntry> entrySet = entryMap.get(entry.getEntryType());
581            entrySet.add(entry);
582        }
583        return entryMap;
584    }
585
586    /**
587     * Returns the {@link Set} of {@link AddeEntry#getGroup() groups} that
588     * match the given {@code address} and {@code type}.
589     * 
590     * @param address ADDE server address whose groups are needed.
591     * Cannot be {@code null}.
592     * @param type Only include groups that match {@link EntryType}.
593     * Cannot be {@code null}.
594     * 
595     * @return Either a set containing the desired groups, or an empty set if
596     * there were no matches.
597     *
598     * @throws NullPointerException if either {@code address} or {@code type}
599     * is {@code null}.
600     */
601    public Set<String> getGroupsFor(final String address, EntryType type) {
602        requireNonNull(address);
603        requireNonNull(type);
604
605        Set<String> groups = newLinkedHashSet(trie.size());
606        groups.addAll(
607            trie.prefixMap(address + '!').values().stream()
608                .filter(e -> e.getAddress().equals(address) && (e.getEntryType() == type))
609                .map(AddeEntry::getGroup)
610                .collect(Collectors.toList()));
611        return groups;
612    }
613
614    /**
615     * Search the server manager for entries that match {@code prefix}.
616     * 
617     * @param prefix {@code String} to match. Cannot be {@code null}.
618     * 
619     * @return {@link List} containing matching entries. If there were no 
620     * matches the {@code List} will be empty.
621     *
622     * @throws NullPointerException if {@code prefix} is {@code null}.
623     *
624     * @see AddeEntry#asStringId()
625     */
626    public List<AddeEntry> searchWithPrefix(final String prefix) {
627        requireNonNull(prefix);
628        return arrList(trie.prefixMap(prefix).values());
629    }
630
631    /**
632     * Returns the {@link Set} of {@link AddeEntry} addresses stored
633     * in this {@code EntryStore}.
634     * 
635     * @return {@code Set} containing all of the stored addresses. If no 
636     * addresses are stored, an empty {@code Set} is returned.
637     */
638    public Set<String> getAddresses() {
639        Set<String> addresses = newLinkedHashSet(trie.size());
640        addresses.addAll(trie.values().stream()
641                             .map(AddeEntry::getAddress)
642                             .collect(Collectors.toList()));
643        return addresses;
644    }
645
646    /**
647     * Returns a {@link Set} containing {@code ADDRESS/GROUPNAME}
648     * {@link String Strings} for each {@link RemoteAddeEntry}.
649     * 
650     * @return The {@literal "entry text"} representations of each 
651     * {@code RemoteAddeEntry}.
652     * 
653     * @see RemoteAddeEntry#getEntryText()
654     */
655    public Set<String> getRemoteEntryTexts() {
656        Set<String> strs = newLinkedHashSet(trie.size());
657        strs.addAll(trie.values().stream()
658                        .filter(entry -> entry instanceof RemoteAddeEntry)
659                        .map(AddeEntry::getEntryText)
660                        .collect(Collectors.toList()));
661        return strs;
662    }
663
664    /**
665     * Returns the {@link Set} of {@literal "groups"} associated with the 
666     * given {@code address}.
667     * 
668     * @param address Address of a server.
669     * 
670     * @return Either all of the {@literal "groups"} on {@code address} or an
671     * empty {@code Set}.
672     */
673    public Set<String> getGroups(final String address) {
674        requireNonNull(address);
675
676        Set<String> groups = newLinkedHashSet(trie.size());
677        groups.addAll(trie.prefixMap(address + '!').values().stream()
678                          .map(AddeEntry::getGroup)
679                          .collect(Collectors.toList()));
680        return groups;
681    }
682
683    /**
684     * Returns the {@link Set} of {@link EntryType EntryTypes} for a given
685     * {@code group} on a given {@code address}.
686     * 
687     * @param address Address of a server.
688     * @param group Group whose {@literal "types"} you want.
689     * 
690     * @return Either of all the types for a given {@code address} and 
691     * {@code group} or an empty {@code Set} if there were no matches.
692     */
693    public Set<EntryType> getTypes(final String address, final String group) {
694        Set<EntryType> types = newLinkedHashSet(trie.size());
695        types.addAll(
696            trie.prefixMap(address + '!' + group + '!').values().stream()
697                .map(AddeEntry::getEntryType)
698                .collect(Collectors.toList()));
699        return types;
700    }
701
702    /**
703     * Searches the set of servers in an attempt to locate the accounting 
704     * information for the matching server. <b>Note</b> that because the data
705     * structure is a {@link Set}, there <i>cannot</i> be duplicate entries,
706     * so there is no need to worry about our criteria finding multiple 
707     * matches.
708     * 
709     * <p>Also note that none of the given parameters accept {@code null} 
710     * values.
711     * 
712     * @param address Address of the server.
713     * @param group Dataset.
714     * @param type Group type.
715     * 
716     * @return Either the {@link AddeAccount} for the given criteria, or 
717     * {@link AddeEntry#DEFAULT_ACCOUNT} if there was no match.
718     * 
719     * @see RemoteAddeEntry#equals(Object)
720     */
721    public AddeAccount getAccountingFor(final String address,
722                                        final String group,
723                                        EntryType type)
724    {
725        Collection<AddeEntry> entries =
726            trie.prefixMap(address+'!'+group+'!'+type.name()).values();
727        for (AddeEntry entry : entries) {
728            if (!isInvalidEntry(entry)) {
729                return entry.getAccount();
730            }
731        }
732        return AddeEntry.DEFAULT_ACCOUNT;
733    }
734
735    /**
736     * Returns the accounting for the given {@code idvServer} and
737     * {@code typeAsStr}.
738     *
739     * @param idvServer Server to search for.
740     * @param typeAsStr One of {@literal "IMAGE"}, {@literal "POINT"},
741     * {@literal "GRID"}, {@literal "TEXT"}, {@literal "NAV"},
742     * {@literal "RADAR"}, {@literal "UNKNOWN"}, or {@literal "INVALID"}.
743     *
744     * @return {@code AddeAccount} associated with {@code idvServer} and
745     * {@code typeAsStr}.
746     */
747    public AddeAccount getAccountingFor(final AddeServer idvServer,
748                                        String typeAsStr)
749    {
750        String address = idvServer.getName();
751        List<AddeServer.Group> groups =
752            (List<AddeServer.Group>)idvServer.getGroups();
753        if ((groups != null) && !groups.isEmpty()) {
754            EntryType type = EntryTransforms.strToEntryType(typeAsStr);
755            return getAccountingFor(address, groups.get(0).getName(), type);
756        } else {
757            return RemoteAddeEntry.DEFAULT_ACCOUNT;
758        }
759    }
760
761    /**
762     * Returns the complete {@link Set} of {@link AddeEntry AddeEntries}.
763     *
764     * @return All of the managed ADDE entries.
765     */
766    public Set<AddeEntry> getEntrySet() {
767        return newLinkedHashSet(trie.values());
768    }
769
770    /**
771     * Returns all non-temporary {@link AddeEntry AddeEntries}.
772     *
773     * @return {@link Set} of ADDE entries that stick around between McIDAS-V
774     * sessions.
775     */
776    public Set<AddeEntry> getPersistedEntrySet() {
777        Set<AddeEntry> entries = newLinkedHashSet(trie.size());
778        entries.addAll(trie.values().stream()
779                           .filter(entry -> !entry.isEntryTemporary())
780                           .collect(Collectors.toList()));
781        return entries;
782    }
783
784    /**
785     * Returns the complete {@link Set} of
786     * {@link RemoteAddeEntry RemoteAddeEntries}.
787     * 
788     * @return {@code Set} of remote ADDE entries stored within the available
789     * entries.
790     */
791    public Set<RemoteAddeEntry> getRemoteEntries() {
792        Set<RemoteAddeEntry> remotes = newLinkedHashSet(trie.size());
793        remotes.addAll(trie.values().stream()
794                           .filter(e -> e instanceof RemoteAddeEntry)
795                           .map(e -> (RemoteAddeEntry) e)
796                           .collect(Collectors.toList()));
797        return remotes;
798    }
799
800    /**
801     * Returns the complete {@link Set} of
802     * {@link LocalAddeEntry LocalAddeEntries}.
803     * 
804     * @return {@code Set} of local ADDE entries  stored within the available
805     * entries.
806     */
807    public Set<LocalAddeEntry> getLocalEntries() {
808        Set<LocalAddeEntry> locals = newLinkedHashSet(trie.size());
809        locals.addAll(trie.prefixMap("localhost").values().stream()
810                          .filter(e -> e instanceof LocalAddeEntry)
811                          .map(e -> (LocalAddeEntry) e)
812                          .collect(Collectors.toList()));
813        return locals;
814    }
815
816    /**
817     * Returns the {@link Set} of {@link LocalAddeEntry LocalAddeEntries} that
818     * will be saved between McIDAS-V sessions.
819     * 
820     * <p>Note: all this does is check {@link LocalAddeEntry#isEntryTemporary()}.
821     * 
822     * @return Local ADDE entries that will be saved for the next session.
823     */
824    public Set<LocalAddeEntry> getPersistedLocalEntries() {
825//        Set<LocalAddeEntry> locals = newLinkedHashSet(trie.size());
826//        for (AddeEntry e : trie.getPrefixedBy("localhost").values()) {
827//            if (e instanceof LocalAddeEntry) {
828//                LocalAddeEntry local = (LocalAddeEntry)e;
829//                if (!local.isEntryTemporary()) {
830//                    locals.add(local);
831//                }
832//            }
833//        }
834//        return locals;
835        return this.filterLocalEntriesByTemporaryStatus(false);
836    }
837
838    /**
839     * Returns any {@link LocalAddeEntry LocalAddeEntries} that will be removed
840     * at the end of the current McIDAS-V session.
841     *
842     * @return {@code Set} of all the temporary local ADDE entries.
843     */
844    public Set<LocalAddeEntry> getTemporaryLocalEntries() {
845        return this.filterLocalEntriesByTemporaryStatus(true);
846    }
847
848    /**
849     * Filters the local entries by whether or not they are set as
850     * {@literal "temporary"}.
851     *
852     * @param getTemporaryEntries {@code true} returns temporary local
853     * entries; {@code false} returns local entries that are permanent.
854     *
855     * @return {@link Set} of filtered local ADDE entries.
856     */
857    private Set<LocalAddeEntry> filterLocalEntriesByTemporaryStatus(
858        final boolean getTemporaryEntries)
859    {
860        Set<LocalAddeEntry> locals = newLinkedHashSet(trie.size());
861        trie.prefixMap("localhost").values().stream()
862            .filter(e -> e instanceof LocalAddeEntry)
863            .forEach(e -> {
864                LocalAddeEntry local = (LocalAddeEntry)e;
865                if (local.isEntryTemporary() == getTemporaryEntries) {
866                    locals.add(local);
867                }
868            });
869        return locals;
870    }
871
872    /**
873     * Removes the given {@link AddeEntry AddeEntries}.
874     *
875     * @param removedEntries {@code AddeEntry} objects to remove.
876     * Cannot be {@code null}.
877     *
878     * @return Whether or not {@code removeEntries} were removed.
879     *
880     * @throws NullPointerException if {@code removedEntries} is {@code null}.
881     */
882    public boolean removeEntries(
883        final Collection<? extends AddeEntry> removedEntries)
884    {
885        requireNonNull(removedEntries);
886
887        boolean val = true;
888        boolean tmp = true;
889        for (AddeEntry e : removedEntries) {
890            if (e.getEntrySource() != EntrySource.SYSTEM) {
891                tmp = trie.remove(e.asStringId()) != null;
892                logger.trace("attempted bulk remove={} status={}", e, tmp);
893                if (!tmp) {
894                    val = tmp;
895                }
896            }
897        }
898        Event evt = val ? Event.REMOVAL : Event.FAILURE;
899        saveEntries();
900        EventBus.publish(evt);
901        return val;
902    }
903
904    /**
905     * Removes a single {@link AddeEntry} from the set of available entries.
906     * 
907     * @param entry Entry to remove. Cannot be {@code null}.
908     * 
909     * @return {@code true} if something was removed, {@code false} otherwise.
910     *
911     * @throws NullPointerException if {@code entry} is {@code null}.
912     */
913    public boolean removeEntry(final AddeEntry entry) {
914        requireNonNull(entry);
915
916        boolean val = trie.remove(entry.asStringId()) != null;
917        logger.trace("attempted remove={} status={}", entry, val);
918        Event evt = val ? Event.REMOVAL : Event.FAILURE;
919        saveEntries();
920        EventBus.publish(evt);
921        return val;
922    }
923
924    /**
925     * Adds a single {@link AddeEntry} to {@link #trie}.
926     * 
927     * @param entry Entry to add. Cannot be {@code null}.
928     * 
929     * @throws NullPointerException if {@code entry} is {@code null}.
930     */
931    public void addEntry(final AddeEntry entry) {
932        requireNonNull(entry, "Cannot add a null entry.");
933
934        trie.put(entry.asStringId(), entry);
935        saveEntries();
936        lastAdded.clear();
937        lastAdded.add(entry);
938        EventBus.publish(Event.ADDITION);
939    }
940
941    /**
942     * Adds a {@link Set} of {@link AddeEntry AddeEntries} to {@link #trie}.
943     * 
944     * @param newEntries New entries to add to the server manager. Cannot be
945     * {@code null}.
946     * 
947     * @throws NullPointerException if {@code newEntries} is {@code null}.
948     */
949    public void addEntries(final Collection<? extends AddeEntry> newEntries) {
950        requireNonNull(newEntries, "Cannot add a null Collection.");
951
952        for (AddeEntry newEntry : newEntries) {
953            trie.put(newEntry.asStringId(), newEntry);
954        }
955        saveEntries();
956        lastAdded.clear();
957        lastAdded.addAll(newEntries);
958        EventBus.publish(Event.ADDITION);
959    }
960
961    /**
962     * Replaces the {@link AddeEntry AddeEntries} within {@code trie} with the
963     * contents of {@code newEntries}.
964     * 
965     * @param oldEntries Entries to be replaced. Cannot be {@code null}.
966     * @param newEntries Entries to use as replacements. Cannot be 
967     * {@code null}.
968     * 
969     * @throws NullPointerException if either of {@code oldEntries} or 
970     * {@code newEntries} is {@code null}.
971     */
972    public void replaceEntries(
973        final Collection<? extends AddeEntry> oldEntries,
974        final Collection<? extends AddeEntry> newEntries)
975    {
976        requireNonNull(oldEntries, "Cannot replace a null Collection.");
977        requireNonNull(newEntries, "Cannot add a null Collection.");
978
979        for (AddeEntry oldEntry : oldEntries) {
980            trie.remove(oldEntry.asStringId());
981        }
982        for (AddeEntry newEntry : newEntries) {
983            trie.put(newEntry.asStringId(), newEntry);
984        }
985        lastAdded.clear();
986        lastAdded.addAll(newEntries); // should probably be more thorough
987        saveEntries();
988        EventBus.publish(Event.REPLACEMENT);
989    }
990
991    /**
992     * Returns all enabled, valid {@link LocalAddeEntry LocalAddeEntries} as a
993     * collection of {@literal "IDV style"} {@link AddeServer.Group}
994     * objects.
995     *
996     * @return {@link Set} of {@code AddeServer.Group} objects that corresponds
997     * with the enabled, valid local ADDE entries.
998     */
999    // if true, filters out disabled local groups; if false, returns all
1000    // local groups
1001    public Set<AddeServer.Group> getIdvStyleLocalGroups() {
1002        Set<LocalAddeEntry> localEntries = getLocalEntries();
1003        Set<AddeServer.Group> idvGroups = newLinkedHashSet(localEntries.size());
1004        for (LocalAddeEntry e : localEntries) {
1005            boolean enabled = e.getEntryStatus() == EntryStatus.ENABLED;
1006            boolean verified = e.getEntryValidity() == EntryValidity.VERIFIED;
1007            if (enabled && verified) {
1008                String group = e.getGroup();
1009                AddeServer.Group idvGroup =
1010                    new AddeServer.Group("IMAGE", group, group);
1011                idvGroups.add(idvGroup);
1012            }
1013        }
1014        return idvGroups;
1015    }
1016
1017    /**
1018     * Returns the entries matching the given {@code server} and
1019     * {@code typeAsStr} parameters as a collection of
1020     * {@link ucar.unidata.idv.chooser.adde.AddeServer.Group AddeServer.Group}
1021     * objects.
1022     *
1023     * @param server Remote ADDE server. Should not be {@code null}.
1024     * @param typeAsStr Entry type. One of {@literal "IMAGE"},
1025     * {@literal "POINT"}, {@literal "GRID"}, {@literal "TEXT"},
1026     * {@literal "NAV"}, {@literal "RADAR"}, {@literal "UNKNOWN"}, or
1027     * {@literal "INVALID"}. Should not be {@code null}.
1028     *
1029     * @return {@link Set} of {@code AddeServer.Group} objects that corresponds
1030     * to the entries associated with {@code server} and {@code typeAsStr}.
1031     */
1032    public Set<AddeServer.Group> getIdvStyleRemoteGroups(
1033        final String server,
1034        final String typeAsStr)
1035    {
1036        return getIdvStyleRemoteGroups(server,
1037            EntryTransforms.strToEntryType(typeAsStr));
1038    }
1039
1040    /**
1041     * Returns the entries matching the given {@code server} and
1042     * {@code type} parameters as a collection of
1043     * {@link AddeServer.Group}
1044     * objects.
1045     *
1046     * @param server Remote ADDE server. Should not be {@code null}.
1047     * @param type Entry type. Should not be {@code null}.
1048     *
1049     * @return {@link Set} of {@code AddeServer.Group} objects that corresponds
1050     * to the entries associated with {@code server} and {@code type}.
1051     */
1052    public Set<AddeServer.Group> getIdvStyleRemoteGroups(final String server,
1053                                                         final EntryType type)
1054    {
1055        Set<AddeServer.Group> idvGroups = newLinkedHashSet(trie.size());
1056        String typeStr = type.name();
1057        for (AddeEntry e : trie.prefixMap(server).values()) {
1058            if (e == RemoteAddeEntry.INVALID_ENTRY) {
1059                continue;
1060            }
1061
1062            boolean enabled = e.getEntryStatus() == EntryStatus.ENABLED;
1063            boolean verified = e.getEntryValidity() == EntryValidity.VERIFIED;
1064            boolean typeMatched = e.getEntryType() == type;
1065            if (enabled && verified && typeMatched) {
1066                String group = e.getGroup();
1067                idvGroups.add(new AddeServer.Group(typeStr, group, group));
1068            }
1069        }
1070        return idvGroups;
1071    }
1072
1073    /**
1074     * Returns a list of all available ADDE datasets, converted to IDV 
1075     * {@link AddeServer} objects.
1076     * 
1077     * @return List of {@code AddeServer} objects for each ADDE entry.
1078     */
1079    public List<AddeServer> getIdvStyleEntries() {
1080        return arrList(EntryTransforms.convertMcvServers(getEntrySet()));
1081    }
1082
1083    /**
1084     * Returns a list that consists of the available ADDE datasets for a given 
1085     * {@link EntryType}, converted to IDV {@link AddeServer} objects.
1086     * 
1087     * @param type Only add entries with this type to the returned list.
1088     * Cannot be {@code null}.
1089     * 
1090     * @return {@code AddeServer} objects for each ADDE entry of the given type.
1091     */
1092    public Set<AddeServer> getIdvStyleEntries(final EntryType type) {
1093        return EntryTransforms.convertMcvServers(getVerifiedEntries(type));
1094    }
1095
1096    /**
1097     * Returns a list that consists of the available ADDE datasets for a given 
1098     * {@link EntryType}, converted to IDV {@link AddeServer} objects.
1099     * 
1100     * @param typeAsStr Only add entries with this type to the returned list. 
1101     * Cannot be {@code null} and must be a value that works with 
1102     * {@link EntryTransforms#strToEntryType(String)}. 
1103     * 
1104     * @return {@code AddeServer} objects for each ADDE entry of the given type.
1105     * 
1106     * @see EntryTransforms#strToEntryType(String)
1107     */
1108    public Set<AddeServer> getIdvStyleEntries(final String typeAsStr) {
1109        return getIdvStyleEntries(EntryTransforms.strToEntryType(typeAsStr));
1110    }
1111
1112    /**
1113     * Process all of the {@literal "IDV-style"} XML resources for a given
1114     * {@literal "source"}.
1115     * 
1116     * @param source Origin of the XML resources.
1117     * @param xmlResources Actual XML resources.
1118     * 
1119     * @return {@link Set} of the {@link AddeEntry AddeEntrys} extracted from
1120     * {@code xmlResources}.
1121     */
1122    private Set<AddeEntry> extractResourceEntries(
1123        EntrySource source,
1124        final XmlResourceCollection xmlResources)
1125    {
1126        Set<AddeEntry> entries = newLinkedHashSet(xmlResources.size());
1127        for (int i = 0; i < xmlResources.size(); i++) {
1128            Element root = xmlResources.getRoot(i);
1129            if (root == null) {
1130                continue;
1131            }
1132            entries.addAll(EntryTransforms.convertAddeServerXml(root, source));
1133        }
1134        return entries;
1135    }
1136
1137    /**
1138     * Process all of the {@literal "user"} XML resources.
1139     * 
1140     * @param xmlResources Resource collection. Cannot be {@code null}.
1141     * 
1142     * @return {@link Set} of {@link RemoteAddeEntry RemoteAddeEntries}
1143     * contained within {@code resource}.
1144     */
1145    private Set<AddeEntry> extractUserEntries(
1146        final XmlResourceCollection xmlResources)
1147    {
1148        int rcSize = xmlResources.size();
1149        Set<AddeEntry> entries = newLinkedHashSet(rcSize);
1150        for (int i = 0; i < rcSize; i++) {
1151            Element root = xmlResources.getRoot(i);
1152            if (root == null) {
1153                continue;
1154            }
1155            entries.addAll(EntryTransforms.convertUserXml(root));
1156        }
1157        return entries;
1158    }
1159
1160    /**
1161     * Returns the path to where the root directory of the user's McIDAS-X 
1162     * binaries <b>should</b> be. <b>The path may be invalid.</b>
1163     * 
1164     * <p>The default path is determined like so:
1165     * <pre>
1166     * System.getProperty("user.dir") + File.separatorChar + "adde"
1167     * </pre>
1168     * 
1169     * <p>Users can provide an arbitrary path at runtime by setting the 
1170     * {@code debug.localadde.rootdir} system property.
1171     * 
1172     * @return {@code String} containing the path to the McIDAS-X root
1173     * directory.
1174     * 
1175     * @see #PROP_DEBUG_LOCALROOT
1176     */
1177    public static String getAddeRootDirectory() {
1178        if (System.getProperties().containsKey(PROP_DEBUG_LOCALROOT)) {
1179            return System.getProperty(PROP_DEBUG_LOCALROOT);
1180        }
1181        String userDir = System.getProperty("user.dir");
1182        Path p;
1183        if (userDir.endsWith("lib") || userDir.endsWith("lib/")) {
1184            p = Paths.get(userDir, "..", "adde");
1185        } else {
1186            p = Paths.get(userDir, "adde");
1187        }
1188        return p.normalize().toString();
1189    }
1190
1191    /**
1192     * Checks the value of the {@code debug.adde.reqs} system property to
1193     * determine whether or not the user has requested ADDE URL debugging 
1194     * output. Output is sent to {@link System#out}.
1195     * 
1196     * <p>Please keep in mind that the {@code debug.adde.reqs} can not 
1197     * force debugging for <i>all</i> ADDE requests. To do so will require
1198     * updates to the VisAD ADDE library.
1199     * 
1200     * @param defValue Value to return if {@code debug.adde.reqs} has
1201     * not been set.
1202     * 
1203     * @return If it exists, the value of {@code debug.adde.reqs}. 
1204     * Otherwise {@code debug.adde.reqs}.
1205     * 
1206     * @see edu.wisc.ssec.mcidas.adde.AddeURL
1207     * @see #PROP_DEBUG_ADDEURL
1208     */
1209    // TODO(jon): this sort of thing should *really* be happening within the 
1210    // ADDE library.
1211    public static boolean isAddeDebugEnabled(final boolean defValue) {
1212        String systemProperty =
1213            System.getProperty(PROP_DEBUG_ADDEURL, Boolean.toString(defValue));
1214        return Boolean.parseBoolean(systemProperty);
1215    }
1216
1217    /**
1218     * Sets the value of the {@code debug.adde.reqs} system property so
1219     * that debugging output can be controlled without restarting McIDAS-V.
1220     * 
1221     * <p>Please keep in mind that the {@code debug.adde.reqs} can not 
1222     * force debugging for <i>all</i> ADDE requests. To do so will require
1223     * updates to the VisAD ADDE library.
1224     * 
1225     * @param value New value of {@code debug.adde.reqs}.
1226     * 
1227     * @return Previous value of {@code debug.adde.reqs}.
1228     * 
1229     * @see edu.wisc.ssec.mcidas.adde.AddeURL
1230     * @see #PROP_DEBUG_ADDEURL
1231     */
1232    public static boolean setAddeDebugEnabled(final boolean value) {
1233        String systemProperty =
1234            System.setProperty(PROP_DEBUG_ADDEURL, Boolean.toString(value));
1235        return Boolean.parseBoolean(systemProperty);
1236    }
1237
1238    /**
1239     * Change the port we are listening on.
1240     * 
1241     * @param port New port number.
1242     */
1243    public static void setLocalPort(final String port) {
1244        localPort = port;
1245    }
1246
1247    /**
1248     * Ask for the port we are listening on.
1249     * 
1250     * @return String representation of the listening port.
1251     */
1252    public static String getLocalPort() {
1253        return localPort;
1254    }
1255
1256    /**
1257     * Get the next port by incrementing current port.
1258     * 
1259     * @return The next port that will be tried.
1260     */
1261    protected static String nextLocalPort() {
1262        return Integer.toString(Integer.parseInt(localPort) + 1);
1263    }
1264
1265    /**
1266     * Starts the local server thread (if it isn't already running).
1267     */
1268    public void startLocalServer() {
1269        if (new File(ADDE_MCSERVL).exists()) {
1270            // Create and start the thread if there isn't already one running
1271            if (!checkLocalServer()) {
1272                if (!testLocalServer()) {
1273                    String logUtil =
1274                        String.format(ERROR_LOGUTIL_USERPATH, USER_DIRECTORY);
1275                    LogUtil.userErrorMessage(logUtil);
1276                    logger.info(ERROR_USERPATH, USER_DIRECTORY);
1277                    return;
1278                }
1279                thread = new AddeThread(this);
1280                thread.start();
1281                EventBus.publish(McservEvent.STARTED);
1282                boolean status = checkLocalServer();
1283                logger.debug("started mcservl? checkLocalServer={}", status);
1284            } else {
1285                logger.debug("mcservl is already running");
1286            }
1287        } else {
1288            logger.debug("invalid path='{}'", ADDE_MCSERVL);
1289        }
1290    }
1291
1292    /**
1293     * Stops the local server thread if it is running.
1294     */
1295    public void stopLocalServer() {
1296        if (checkLocalServer()) {
1297            //TODO: stopProcess (actually Process.destroy()) hangs on Macs...
1298            //      doesn't seem to kill the children properly
1299            if (!McIDASV.isMac()) {
1300                thread.stopProcess();
1301            }
1302
1303            thread.interrupt();
1304            thread = null;
1305            EventBus.publish(McservEvent.STOPPED);
1306            boolean status = checkLocalServer();
1307            logger.debug("stopped mcservl? checkLocalServer={}", status);
1308        } else {
1309            logger.debug("mcservl is not running.");
1310        }
1311    }
1312    
1313    /**
1314     * Test to see if the thread can access userpath
1315     * 
1316     * @return {@code true} if the local server can access userpath,
1317     * {@code false} otherwise.
1318     */
1319    public boolean testLocalServer() {
1320        String[] cmds = { ADDE_MCSERVL, "-t" };
1321        String[] env = McIDASV.isWindows()
1322                       ? getWindowsAddeEnv()
1323                       : getUnixAddeEnv();
1324
1325        try {
1326            Process proc = Runtime.getRuntime().exec(cmds, env);
1327            int result = proc.waitFor();
1328            if (result != 0) {
1329                return false;
1330            }
1331        } catch (Exception e) {
1332            return false;
1333        }
1334        return true;
1335    }
1336
1337    /**
1338     * Check to see if the thread is running.
1339     * 
1340     * @return {@code true} if the local server thread is running;
1341     * {@code false} otherwise.
1342     */
1343    public boolean checkLocalServer() {
1344        return (thread != null) && thread.isAlive();
1345    }
1346}