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