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