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