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