001    /*
002     * $Id: EntryStore.java,v 1.65 2012/02/19 17:35:48 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    package edu.wisc.ssec.mcidasv.servermanager;
031    
032    import static edu.wisc.ssec.mcidasv.util.Contract.notNull;
033    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
034    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast;
035    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashMap;
036    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
037    
038    import java.io.File;
039    import java.io.IOException;
040    import java.util.*;
041    
042    import org.bushe.swing.event.EventBus;
043    import org.bushe.swing.event.annotation.AnnotationProcessor;
044    import org.bushe.swing.event.annotation.EventSubscriber;
045    
046    import org.slf4j.Logger;
047    import org.slf4j.LoggerFactory;
048    
049    import org.w3c.dom.Element;
050    
051    import ucar.unidata.idv.IdvObjectStore;
052    import ucar.unidata.idv.IdvResourceManager;
053    import ucar.unidata.idv.chooser.adde.AddeServer;
054    import ucar.unidata.xml.XmlResourceCollection;
055    
056    import edu.wisc.ssec.mcidasv.Constants;
057    import edu.wisc.ssec.mcidasv.McIDASV;
058    import edu.wisc.ssec.mcidasv.ResourceManager;
059    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
060    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
061    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
062    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
063    import edu.wisc.ssec.mcidasv.servermanager.AddeThread.McservEvent;
064    import edu.wisc.ssec.mcidasv.util.trie.CharSequenceKeyAnalyzer;
065    import edu.wisc.ssec.mcidasv.util.trie.PatriciaTrie;
066    
067    public class EntryStore {
068    
069        /** 
070         * Property that allows users to supply arbitrary paths to McIDAS-X 
071         * binaries used by mcservl.
072         * 
073         * @see #getAddeRootDirectory()
074         */
075        private static final String PROP_DEBUG_LOCALROOT = "debug.localadde.rootdir";
076    
077        /**
078         * Property that allows users to control debug output from ADDE requests.
079         * 
080         * @see #isAddeDebugEnabled(boolean)
081         * @see #setAddeDebugEnabled(boolean)
082         */
083        private static final String PROP_DEBUG_ADDEURL = "debug.adde.reqs";
084    
085        /** Enumeration of the various server manager events. */
086        public enum Event { 
087            /** Entries were replaced. */
088            REPLACEMENT, 
089            /** Entries were removed. */
090            REMOVAL, 
091            /** Entries were added. */
092            ADDITION, 
093            /** Entries were updated.*/
094            UPDATE, 
095            /** Something failed! */
096            FAILURE, 
097            /** Local servers started. */
098            STARTED, 
099            /** Catch-all? */
100            UNKNOWN 
101        }
102    
103        /** Logging object. */
104        private static final Logger logger = LoggerFactory.getLogger(EntryStore.class);
105    
106        private static final String PREF_ADDE_ENTRIES = "mcv.servers.entries";
107    
108        /** The ADDE servers known to McIDAS-V. */
109        private final PatriciaTrie<String, AddeEntry> trie;
110    
111        /** {@literal "Root"} local server directory. */
112        private final String ADDE_DIRECTORY;
113    
114        /** Path to local server binaries. */
115        private final String ADDE_BIN;
116    
117        /** Path to local server data. */
118        private final String ADDE_DATA;
119    
120        /** Path to mcservl. */
121        private final String ADDE_MCSERVL;
122    
123        /** Path to the user's {@literal "userpath"} directory. */
124        private final String USER_DIRECTORY;
125    
126        /** Path to the user's {@literal "RESOLV.SRV"}. */
127        private final String ADDE_RESOLV;
128    
129        /** */
130        private final String MCTRACE;
131    
132        /** Which port is this particular manager operating on */
133        private static String localPort;
134    
135        /** Thread that monitors the mcservl process. */
136        private static AddeThread thread;
137    
138        /** The last {@link AddeEntry}s added to the manager. */
139        private final List<AddeEntry> lastAdded;
140    
141        private final IdvObjectStore idvStore;
142    
143        private boolean restartingMcserv;
144    
145        /**
146         * Constructs a server manager.
147         * 
148         * @param store 
149         * @param rscManager 
150         */
151        public EntryStore(final IdvObjectStore store, final IdvResourceManager rscManager) {
152            notNull(store);
153            notNull(rscManager);
154    
155            this.idvStore = store;
156            this.trie = new PatriciaTrie<String, AddeEntry>(new CharSequenceKeyAnalyzer());
157            this.ADDE_DIRECTORY = getAddeRootDirectory();
158            this.ADDE_BIN = ADDE_DIRECTORY + File.separator + "bin";
159            this.ADDE_DATA = ADDE_DIRECTORY + File.separator + "data";
160            this.localPort = Constants.LOCAL_ADDE_PORT;
161            this.restartingMcserv = false;
162            this.lastAdded = arrList();
163            AnnotationProcessor.process(this);
164    
165            McIDASV mcv = McIDASV.getStaticMcv();
166            USER_DIRECTORY = mcv.getUserDirectory();
167            ADDE_RESOLV = mcv.getUserFile("RESOLV.SRV");
168            MCTRACE = "0";
169    
170            if (McIDASV.isWindows()) {
171                ADDE_MCSERVL = ADDE_BIN + "\\mcservl.exe";
172            } else {
173                ADDE_MCSERVL = ADDE_BIN + "/mcservl";
174            }
175    
176            try {
177                Set<LocalAddeEntry> locals = EntryTransforms.readResolvFile(ADDE_RESOLV);
178                putEntries(trie, locals);
179            } catch (IOException e) {
180                logger.warn("EntryStore: RESOLV.SRV missing; expected=\"" + ADDE_RESOLV + '"');
181            }
182    
183            XmlResourceCollection userResource = rscManager.getXmlResources(ResourceManager.RSC_NEW_USERSERVERS);
184            XmlResourceCollection sysResource = rscManager.getXmlResources(IdvResourceManager.RSC_ADDESERVER);
185            putEntries(trie, extractFromPreferences(store));
186            putEntries(trie, extractUserEntries(userResource));
187            putEntries(trie, extractResourceEntries(EntrySource.SYSTEM, sysResource));
188        }
189    
190        /**
191         * 
192         * 
193         * @param trie Cannot be {@code null}.
194         * @param newEntries Cannot be {@code null}.
195         */
196        private static void putEntries(final PatriciaTrie<String, AddeEntry> trie, final Collection<? extends AddeEntry> newEntries) {
197            notNull(trie);
198            notNull(newEntries);
199            for (AddeEntry e : newEntries) {
200                trie.put(e.asStringId(), e);
201            }
202        }
203    
204        public IdvObjectStore getIdvStore() {
205            return idvStore;
206        }
207    
208        protected String[] getWindowsAddeEnv() {
209            // Drive letters should come from environment
210            // Java drive is not necessarily system drive
211            return new String[] {
212                "PATH=" + ADDE_BIN,
213                "MCPATH=" + USER_DIRECTORY+':'+ADDE_DATA,
214                "MCNOPREPEND=1",
215                "MCTRACE=" + MCTRACE,
216                "MCJAVAPATH=" + System.getProperty("java.home"),
217                "MCBUFRJARPATH=" + ADDE_BIN,
218                "SYSTEMDRIVE=" + System.getenv("SystemDrive"),
219                "SYSTEMROOT=" + System.getenv("SystemRoot"),
220                "HOMEDRIVE=" + System.getenv("HOMEDRIVE"),
221                "HOMEPATH=\\Windows"
222            };
223        }
224    
225        protected String[] getUnixAddeEnv() {
226            return new String[] {
227                "PATH=" + ADDE_BIN,
228                "MCPATH=" + USER_DIRECTORY+':'+ADDE_DATA,
229                "LD_LIBRARY_PATH=" + ADDE_BIN,
230                "DYLD_LIBRARY_PATH=" + ADDE_BIN,
231                "MCNOPREPEND=1",
232                "MCTRACE=" + MCTRACE,
233                "MCJAVAPATH=" + System.getProperty("java.home"),
234                "MCBUFRJARPATH=" + ADDE_BIN
235            };
236        }
237    
238        protected String[] getAddeCommands() {
239            return new String[] { ADDE_MCSERVL, "-p", localPort, "-v" };
240        }
241    
242        /**
243         * Determine the validity of a given {@link edu.wisc.ssec.mcidasv.servermanager.AddeEntry AddeEntry}.
244         * 
245         * @param entry Entry to check. Cannot be {@code null}.
246         * 
247         * @return {@code true} if {@code entry} is invalid or {@code false} otherwise.
248         * 
249         * @throws AssertionError if {@code entry} is somehow neither a {@code RemoteAddeEntry} or {@code LocalAddeEntry}.
250         * 
251         * @see edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry#INVALID_ENTRY
252         * @see edu.wisc.ssec.mcidasv.servermanager.RemoteAddeEntry#INVALID_ENTRY
253         */
254        public static boolean isInvalidEntry(final AddeEntry entry) {
255            notNull(entry);
256            boolean retVal = true;
257            if (entry instanceof RemoteAddeEntry) {
258                retVal = RemoteAddeEntry.INVALID_ENTRY.equals(entry);
259            } else if (entry instanceof LocalAddeEntry) {
260                retVal = LocalAddeEntry.INVALID_ENTRY.equals(entry);
261            } else {
262                throw new AssertionError("Unknown AddeEntry type: "+entry.getClass().getName());
263            }
264            return retVal;
265        }
266    
267        /**
268         * Returns the {@link edu.wisc.ssec.mcidasv.servermanager.AddeEntry AddeEntrys} stored 
269         * in the user's preferences.
270         * 
271         * @param store Object store that represents the user's preferences. Cannot be {@code null}.
272         * 
273         * @return Either the {@code AddeEntrys} stored in the prefs or an empty {@link java.util.Set Set}.
274         */
275        private Set<AddeEntry> extractFromPreferences(final IdvObjectStore store) {
276            assert store != null;
277    
278            // this is valid--the only thing ever written to 
279            // PREF_REMOTE_ADDE_ENTRIES is an ArrayList of RemoteAddeEntry objects.
280            @SuppressWarnings("unchecked")
281            List<AddeEntry> asList = 
282                (List<AddeEntry>)store.get(PREF_ADDE_ENTRIES);
283            Set<AddeEntry> entries;
284            if (asList == null) {
285                entries = Collections.emptySet();
286            } else {
287                entries = newLinkedHashSet(asList.size());
288                for (AddeEntry entry : asList) {
289                    if (entry instanceof RemoteAddeEntry) {
290                        entries.add(entry);
291                    }
292                }
293            }
294            return entries;
295        }
296    
297        /**
298         * Responds to server manager events being passed with the event bus. 
299         * 
300         * @param evt Event to which this method is responding.
301         */
302        @EventSubscriber(eventClass=Event.class)
303        public void onEvent(Event evt) {
304            notNull(evt);
305            saveEntries();
306        }
307    
308        /**
309         * Saves the current set of ADDE servers to the user's preferences.
310         */
311        public void saveEntries() {
312            idvStore.put(PREF_ADDE_ENTRIES, arrList(trie.values()));
313            idvStore.saveIfNeeded();
314            try {
315                EntryTransforms.writeResolvFile(ADDE_RESOLV, getLocalEntries());
316            } catch (IOException e) {
317                logger.error("EntryStore: RESOLV.SRV missing; expected=\""+ADDE_RESOLV+"\"");
318            }
319        }
320    
321        /**
322         * Searches the newest entries for the entries of the given {@link edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType EntryType}.
323         * 
324         * @param type Look for entries matching this {@code EntryType}. Cannot be {@code null}.
325         * 
326         * @return Either a {@link java.util.List List} of entries or an empty {@code List}.
327         *
328         * @throws NullPointerException if {@code type} is {@code null}.
329         */
330        public List<AddeEntry> getLastAddedByType(final EntryType type) {
331            notNull(type);
332            List<AddeEntry> entries = arrList();
333            for (AddeEntry entry : lastAdded) {
334                if (entry.getEntryType() == type) {
335                    entries.add(entry);
336                }
337            }
338            return entries;
339        }
340    
341        public List<AddeEntry> getLastAddedByTypes(final EnumSet<EntryType> types) {
342            notNull(types);
343            List<AddeEntry> entries = arrList();
344            for (AddeEntry entry : lastAdded) {
345                if (types.contains(entry.getEntryType())) {
346                    entries.add(entry);
347                }
348            }
349            return entries;
350        }
351    
352        public List<AddeEntry> getLastAdded() {
353            return arrList(lastAdded);
354        }
355    
356        /**
357         * Returns the {@link Set} of {@link AddeEntry}s that are known to work (for
358         * a given {@link EntryType} of entries).
359         * 
360         * @param type The {@code EntryType} you are interested in.
361         * 
362         * @return A {@code Set} of matching {@code RemoteAddeEntry}s. If there 
363         * were no matches, an empty {@code Set} is returned.
364         */
365        public Set<AddeEntry> getVerifiedEntries(final EntryType type) {
366            notNull(type);
367            Set<AddeEntry> verified = newLinkedHashSet(trie.size());
368            for (AddeEntry entry : trie.values()) {
369                if (entry.getEntryType() != type)
370                    continue;
371    
372                if (entry instanceof LocalAddeEntry) {
373                    verified.add(entry);
374                } else if (entry.getEntryValidity() == EntryValidity.VERIFIED) {
375                    verified.add(entry);
376                }
377            }
378            return verified;
379        }
380    
381        // TODO(jon): better name
382        public Map<EntryType, Set<AddeEntry>> getVerifiedEntriesByTypes() {
383            Map<EntryType, Set<AddeEntry>> entryMap =
384                    newLinkedHashMap(EntryType.values().length);
385            int size = trie.size();
386            for (EntryType type : EntryType.values()) {
387                entryMap.put(type, new LinkedHashSet<AddeEntry>(size));
388            }
389    
390            for (AddeEntry entry : trie.values()) {
391                Set<AddeEntry> entrySet = entryMap.get(entry.getEntryType());
392                entrySet.add(entry);
393            }
394            return entryMap;
395        }
396    
397        /**
398         * Returns the {@link Set} of {@link AddeEntry#getGroup()}s
399         * that match the given {@code address} and {@code type}.
400         * 
401         * @param address ADDE server address whose groups are needed.
402         * Cannot be {@code null}.
403         * @param type Only include groups that match {@link EntryType}.
404         * Cannot be {@code null}.
405         * 
406         * @return Either a set containing the desired groups, or an empty set if
407         * there were no matches.
408         */
409        public Set<String> getGroupsFor(final String address, EntryType type) {
410            notNull(address);
411            notNull(type);
412            Set<String> groups = newLinkedHashSet(trie.size());
413            for (AddeEntry entry : trie.getPrefixedBy(address+'!').values()) {
414                if (entry.getAddress().equals(address) && entry.getEntryType() == type) {
415                    groups.add(entry.getGroup());
416                }
417            }
418            return groups;
419        }
420    
421        /**
422         * Search the server manager for entries that match {@code prefix}.
423         * 
424         * @param prefix {@code String} to match.
425         * 
426         * @return {@link List} containing matching entries. If there were no 
427         * matches the {@code List} will be empty.
428         * 
429         * @see AddeEntry#asStringId()
430         */
431        public List<AddeEntry> searchWithPrefix(final String prefix) {
432            notNull(prefix);
433            return arrList(trie.getPrefixedBy(prefix).values());
434        }
435    
436        /**
437         * Returns the {@link Set} of {@link AddeEntry} addresses stored
438         * in this {@code EntryStore}.
439         * 
440         * @return {@code Set} containing all of the stored addresses. If no 
441         * addresses are stored, an empty {@code Set} is returned.
442         */
443        public Set<String> getAddresses() {
444            Set<String> addresses = newLinkedHashSet(trie.size());
445            for (AddeEntry entry : trie.values()) {
446                addresses.add(entry.getAddress());
447            }
448            return addresses;
449        }
450    
451        /**
452         * Returns a {@link Set} containing <b>ADDRESS/GROUPNAME</b> {@code String}s
453         * for each {@link RemoteAddeEntry}.
454         * 
455         * @return The {@literal "entry text"} representations of each 
456         * {@code RemoteAddeEntry}.
457         * 
458         * @see RemoteAddeEntry#getEntryText()
459         */
460        public Set<String> getRemoteEntryTexts() {
461            Set<String> strs = newLinkedHashSet(trie.size());
462            for (AddeEntry entry : trie.values()) {
463                if (entry instanceof RemoteAddeEntry) {
464                    strs.add(entry.getEntryText());
465                }
466            }
467            return strs;
468        }
469    
470        /**
471         * Returns the {@link Set} of {@literal "groups"} associated with the 
472         * given {@code address}.
473         * 
474         * @param address Address of a server.
475         * 
476         * @return Either all of the {@literal "groups"} on {@code address} or an
477         * empty {@code Set}.
478         */
479        public Set<String> getGroups(final String address) {
480            notNull(address);
481            Set<String> groups = newLinkedHashSet(trie.size());
482            for (AddeEntry entry : trie.getPrefixedBy(address+'!').values()) {
483                groups.add(entry.getGroup());
484            }
485            return groups;
486        }
487    
488        /**
489         * Returns the {@link Set} of {@link EntryType}s for a given {@code group}
490         * on a given {@code address}.
491         * 
492         * @param address Address of a server.
493         * @param group Group whose {@literal "types"} you want.
494         * 
495         * @return Either of all the types for a given {@code address} and 
496         * {@code group} or an empty {@code Set} if there were no matches.
497         */
498        public Set<EntryType> getTypes(final String address, final String group) {
499            Set<EntryType> types = newLinkedHashSet(trie.size());
500            for (AddeEntry entry : trie.getPrefixedBy(address+'!'+group+'!').values()) {
501                types.add(entry.getEntryType());
502            }
503            return types;
504        }
505    
506        /**
507         * Searches the set of servers in an attempt to locate the accounting 
508         * information for the matching server. <b>Note</b> that because the data
509         * structure is a {@link Set}, there <i>cannot</i> be duplicate entries,
510         * so there is no need to worry about our criteria finding multiple 
511         * matches.
512         * 
513         * <p>Also note that none of the given parameters accept {@code null} 
514         * values.
515         * 
516         * @param address Address of the server.
517         * @param group Dataset.
518         * @param type Group type.
519         * 
520         * @return Either the {@link AddeAccount} for the given criteria, or 
521         * {@link AddeEntry#DEFAULT_ACCOUNT} if there was no match.
522         * 
523         * @see RemoteAddeEntry#equals(Object)
524         */
525        public AddeAccount getAccountingFor(final String address, final String group, EntryType type) {
526            Collection<AddeEntry> entries = trie.getPrefixedBy(address+'!'+group+'!'+type.name()).values();
527            for (AddeEntry entry : entries) {
528                if (!isInvalidEntry(entry)) {
529                    return entry.getAccount();
530                }
531            }
532            return AddeEntry.DEFAULT_ACCOUNT;
533        }
534    
535        public AddeAccount getAccountingFor(final AddeServer idvServer, String typeAsStr) {
536            String address = idvServer.getName();
537            List<AddeServer.Group> groups = cast(idvServer.getGroups());
538            if (groups != null && !groups.isEmpty()) {
539                EntryType type = EntryTransforms.strToEntryType(typeAsStr);
540                return getAccountingFor(address, groups.get(0).getName(), type);
541            } else {
542                return RemoteAddeEntry.DEFAULT_ACCOUNT;
543            }
544        }
545    
546        /**
547         * Returns the complete {@link Set} of {@link AddeEntry}s.
548         */
549        public Set<AddeEntry> getEntrySet() {
550            return newLinkedHashSet(trie.values());
551        }
552    
553        /**
554         * Returns the complete {@link Set} of {@link RemoteAddeEntry}s.
555         * 
556         * @return The {@code RemoteAddeEntry}s stored within the available entries.
557         */
558        public Set<RemoteAddeEntry> getRemoteEntries() {
559            Set<RemoteAddeEntry> remotes = newLinkedHashSet(trie.size());
560            for (AddeEntry e : trie.values()) {
561                if (e instanceof RemoteAddeEntry) {
562                    remotes.add((RemoteAddeEntry)e);
563                }
564            }
565            return remotes;
566        }
567    
568        /**
569         * Returns the complete {@link Set} of {@link LocalAddeEntry}s.
570         * 
571         * @return The {@code LocalAddeEntry}s stored within the available entries.
572         */
573        public Set<LocalAddeEntry> getLocalEntries() {
574            Set<LocalAddeEntry> locals = newLinkedHashSet(trie.size());
575            for (AddeEntry e : trie.getPrefixedBy("localhost").values()) {
576                if (e instanceof LocalAddeEntry) {
577                    locals.add((LocalAddeEntry)e);
578                }
579            }
580            return locals;
581        }
582    
583        public boolean removeEntries(
584            final Collection<? extends AddeEntry> removedEntries) 
585        {
586            notNull(removedEntries);
587    
588            boolean val = true;
589            boolean tmpVal = true;
590            for (AddeEntry entry : removedEntries) {
591                if (entry.getEntrySource() != EntrySource.SYSTEM) {
592                    tmpVal = (trie.remove(entry.asStringId()) != null);
593                    logger.trace("attempted bulk remove={} status={}", entry, tmpVal);
594                    if (!tmpVal) {
595                        val = tmpVal;
596                    }
597                }
598            }
599            Event evt = (val) ? Event.REMOVAL : Event.FAILURE; 
600            saveEntries();
601            EventBus.publish(evt);
602            return val;
603        }
604    
605        /**
606         * Removes a single {@link AddeEntry} from the set of available entries.
607         * 
608         * @param entry Entry to remove. Cannot be {@code null}.
609         * 
610         * @return {@code true} if something was removed, {@code false} otherwise.
611         */
612        public boolean removeEntry(final AddeEntry entry) {
613            notNull(entry);
614            boolean val = (trie.remove(entry.asStringId()) != null);
615            logger.trace("attempted remove={} status={}", entry, val);
616            Event evt = (val) ? Event.REMOVAL : Event.FAILURE;
617            saveEntries();
618            EventBus.publish(evt);
619            return val;
620        }
621    
622        /**
623         * Adds a {@link Set} of {@link AddeEntry}s to {@link #trie}.
624         * 
625         * @param newEntries New entries to add to the server manager. Cannot be
626         * {@code null}.
627         * 
628         * @throws NullPointerException if {@code newEntries} is {@code null}.
629         */
630        public void addEntries(final Collection<? extends AddeEntry> newEntries) {
631            notNull(newEntries, "Cannot add a null set");
632            for (AddeEntry newEntry : newEntries) {
633                trie.put(newEntry.asStringId(), newEntry);
634            }
635            saveEntries();
636            lastAdded.clear();
637            lastAdded.addAll(newEntries);
638            EventBus.publish(Event.ADDITION);
639        }
640    
641        /**
642         * Replaces the {@link AddeEntry}s within {@code trie} with the contents
643         * of {@code newEntries}.
644         * 
645         * @param oldEntries Entries to be replaced. Cannot be {@code null}.
646         * @param newEntries Entries to use as replacements. Cannot be 
647         * {@code null}.
648         * 
649         * @throws NullPointerException if either of {@code oldEntries} or 
650         * {@code newEntries} is {@code null}.
651         */
652        public void replaceEntries(final Collection<? extends AddeEntry> oldEntries, final Collection<? extends AddeEntry> newEntries) {
653            notNull(oldEntries, "Cannot replace a null set");
654            notNull(newEntries, "Cannot add a null set");
655    
656            for (AddeEntry oldEntry : oldEntries) {
657                trie.remove(oldEntry.asStringId());
658            }
659            for (AddeEntry newEntry : newEntries) {
660                trie.put(newEntry.asStringId(), newEntry);
661            }
662            lastAdded.clear();
663            lastAdded.addAll(newEntries); // should probably be more thorough
664            saveEntries();
665            EventBus.publish(Event.REPLACEMENT);
666        }
667    
668        // if true, filters out disabled local groups; if false, returns all local groups
669        public Set<AddeServer.Group> getIdvStyleLocalGroups() {
670            Set<LocalAddeEntry> localEntries = getLocalEntries();
671            Set<AddeServer.Group> idvGroups = newLinkedHashSet(localEntries.size());
672            for (LocalAddeEntry entry : localEntries) {
673                if (entry.getEntryStatus() == EntryStatus.ENABLED && entry.getEntryValidity() == EntryValidity.VERIFIED) {
674                    String group = entry.getGroup();
675                    AddeServer.Group idvGroup = new AddeServer.Group("IMAGE", group, group);
676                    idvGroups.add(idvGroup);
677                }
678            }
679            return idvGroups;
680        }
681    
682        public Set<AddeServer.Group> getIdvStyleRemoteGroups(final String server, final String typeAsStr) {
683            return getIdvStyleRemoteGroups(server, EntryTransforms.strToEntryType(typeAsStr));
684        }
685    
686        public Set<AddeServer.Group> getIdvStyleRemoteGroups(final String server, final EntryType type) {
687            Set<AddeServer.Group> idvGroups = newLinkedHashSet(trie.size());
688            String typeStr = type.name();
689            for (AddeEntry matched : trie.getPrefixedBy(server).values()) {
690                if (matched == RemoteAddeEntry.INVALID_ENTRY) {
691                    continue;
692                }
693    
694                if (matched.getEntryStatus() == EntryStatus.ENABLED && matched.getEntryValidity() == EntryValidity.VERIFIED && matched.getEntryType() == type) {
695                    String group = matched.getGroup();
696                    idvGroups.add(new AddeServer.Group(typeStr, group, group));
697                }
698            }
699            return idvGroups;
700        }
701    
702        /**
703         * Returns a list of all available ADDE datasets, converted to IDV 
704         * {@link AddeServer} objects.
705         * 
706         * @return List of {@code AddeServer} objects for each ADDE entry.
707         */
708        public List<AddeServer> getIdvStyleEntries() {
709            return arrList(EntryTransforms.convertMcvServers(getEntrySet()));
710        }
711    
712        /**
713         * Returns a list that consists of the available ADDE datasets for a given 
714         * {@link EntryType}, converted to IDV {@link AddeServer} objects.
715         * 
716         * @param type Only add entries with this type to the returned list. Cannot be {@code null}. 
717         * 
718         * @return {@code AddeServer} objects for each ADDE entry of the given type.
719         */
720        public Set<AddeServer> getIdvStyleEntries(final EntryType type) {
721            return EntryTransforms.convertMcvServers(getVerifiedEntries(type));
722        }
723    
724        /**
725         * Returns a list that consists of the available ADDE datasets for a given 
726         * {@link EntryType}, converted to IDV {@link AddeServer} objects.
727         * 
728         * @param typeAsStr Only add entries with this type to the returned list. 
729         * Cannot be {@code null} and must be a value that works with 
730         * {@link EntryTransforms#strToEntryType(String)}. 
731         * 
732         * @return {@code AddeServer} objects for each ADDE entry of the given type.
733         * 
734         * @see EntryTransforms#strToEntryType(String)
735         */
736        public Set<AddeServer> getIdvStyleEntries(final String typeAsStr) {
737            return getIdvStyleEntries(EntryTransforms.strToEntryType(typeAsStr));
738        }
739    
740        /**
741         * Process all of the {@literal "IDV-style"} XML resources for a given
742         * {@literal "source"}.
743         * 
744         * @param source Origin of the XML resources.
745         * @param xmlResources Actual XML resources.
746         * 
747         * @return {@link Set} of the {@link AddeEntry AddeEntrys} extracted from
748         * {@code xmlResources}.
749         */
750        private Set<AddeEntry> extractResourceEntries(EntrySource source, final XmlResourceCollection xmlResources) {
751            Set<AddeEntry> entries = newLinkedHashSet(xmlResources.size());
752            for (int i = 0; i < xmlResources.size(); i++) {
753                Element root = xmlResources.getRoot(i);
754                if (root == null) {
755                    continue;
756                }
757                entries.addAll(EntryTransforms.convertAddeServerXml(root, source));
758            }
759            return entries;
760        }
761    
762        /**
763         * Process all of the {@literal "user"} XML resources.
764         * 
765         * @param xmlResources Resource collection. Cannot be {@code null}.
766         * 
767         * @return {@link Set} of {@link RemoteAddeEntry}s contained within 
768         * {@code resource}.
769         */
770        private Set<AddeEntry> extractUserEntries(final XmlResourceCollection xmlResources) {
771            int rcSize = xmlResources.size();
772            Set<AddeEntry> entries = newLinkedHashSet(rcSize);
773            for (int i = 0; i < rcSize; i++) {
774                Element root = xmlResources.getRoot(i);
775                if (root == null) {
776                    continue;
777                }
778                entries.addAll(EntryTransforms.convertUserXml(root));
779            }
780            return entries;
781        }
782    
783        /**
784         * Returns the path to where the root directory of the user's McIDAS-X 
785         * binaries <b>should</b> be. <b>The path may be invalid.</b>
786         * 
787         * <p>The default path is determined like so:
788         * <pre>
789         * System.getProperty("user.dir") + File.separatorChar + "adde"
790         * </pre>
791         * 
792         * <p>Users can provide an arbitrary path at runtime by setting the 
793         * {@code debug.localadde.rootdir} system property.
794         * 
795         * @return {@code String} containing the path to the McIDAS-X root directory. 
796         * 
797         * @see #PROP_DEBUG_LOCALROOT
798         */
799        public static String getAddeRootDirectory() {
800            if (System.getProperties().containsKey(PROP_DEBUG_LOCALROOT)) {
801                return System.getProperty(PROP_DEBUG_LOCALROOT);
802            }
803            return System.getProperty("user.dir") + File.separatorChar + "adde";
804        }
805    
806        /**
807         * Checks the value of the {@code debug.adde.reqs} system property to
808         * determine whether or not the user has requested ADDE URL debugging 
809         * output. Output is sent to {@link System#out}.
810         * 
811         * <p>Please keep in mind that the {@code debug.adde.reqs} can not 
812         * force debugging for <i>all</i> ADDE requests. To do so will require
813         * updates to the VisAD ADDE library.
814         * 
815         * @param defaultValue Value to return if {@code debug.adde.reqs} has
816         * not been set.
817         * 
818         * @return If it exists, the value of {@code debug.adde.reqs}. 
819         * Otherwise {@code debug.adde.reqs}.
820         * 
821         * @see edu.wisc.ssec.mcidas.adde.AddeURL
822         * @see #PROP_DEBUG_ADDEURL
823         */
824        // TODO(jon): this sort of thing should *really* be happening within the 
825        // ADDE library.
826        public static boolean isAddeDebugEnabled(final boolean defaultValue) {
827            return Boolean.parseBoolean(System.getProperty(PROP_DEBUG_ADDEURL, Boolean.toString(defaultValue)));
828        }
829    
830        /**
831         * Sets the value of the {@code debug.adde.reqs} system property so
832         * that debugging output can be controlled without restarting McIDAS-V.
833         * 
834         * <p>Please keep in mind that the {@code debug.adde.reqs} can not 
835         * force debugging for <i>all</i> ADDE requests. To do so will require
836         * updates to the VisAD ADDE library.
837         * 
838         * @param value New value of {@code debug.adde.reqs}.
839         * 
840         * @return Previous value of {@code debug.adde.reqs}.
841         * 
842         * @see edu.wisc.ssec.mcidas.adde.AddeURL
843         * @see #PROP_DEBUG_ADDEURL
844         */
845        public static boolean setAddeDebugEnabled(final boolean value) {
846            return Boolean.parseBoolean(System.setProperty(PROP_DEBUG_ADDEURL, Boolean.toString(value)));
847        }
848    
849        /**
850         * Change the port we are listening on.
851         * 
852         * @param port New port number.
853         */
854        public static void setLocalPort(final String port) {
855            localPort = port;
856        }
857    
858        /**
859         * Ask for the port we are listening on.
860         * 
861         * @return String representation of the listening port.
862         */
863        public static String getLocalPort() {
864            return localPort;
865        }
866    
867        /**
868         * Get the next port by incrementing current port.
869         * 
870         * @return The next port that will be tried.
871         */
872        protected static String nextLocalPort() {
873            return Integer.toString(Integer.parseInt(localPort) + 1);
874        }
875    
876        /**
877         * Starts the local server thread (if it isn't already running).
878         */
879        public void startLocalServer() {
880            if ((new File(ADDE_MCSERVL)).exists()) {
881                // Create and start the thread if there isn't already one running
882                if (!checkLocalServer()) {
883                    thread = new AddeThread(this);
884                    thread.start();
885                    EventBus.publish(McservEvent.STARTED);
886                    logger.debug("started mcservl? checkLocalServer={}", checkLocalServer());
887                } else {
888                    logger.debug("mcservl is already running");
889                }
890            } else {
891                logger.debug("invalid path='{}'", ADDE_MCSERVL);
892            }
893        }
894    
895        /**
896         * Stops the local server thread if it is running.
897         */
898        public void stopLocalServer() {
899            if (checkLocalServer()) {
900                //TODO: stopProcess (actually Process.destroy()) hangs on Macs...
901                //      doesn't seem to kill the children properly
902                if (!McIDASV.isMac()) {
903                    thread.stopProcess();
904                }
905    
906                thread.interrupt();
907                thread = null;
908                EventBus.publish(McservEvent.STOPPED);
909                logger.debug("stopped mcservl? checkLocalServer={}", checkLocalServer());
910            } else {
911                logger.debug("mcservl is not running.");
912            }
913        }
914    
915        /**
916         * Check to see if the thread is running.
917         * 
918         * @return {@code true} if the local server thread is running; {@code false} otherwise.
919         */
920        public boolean checkLocalServer() {
921            if (thread != null && thread.isAlive()) {
922                return true;
923            } else {
924                return false;
925            }
926        }
927    }