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