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