001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028
029package edu.wisc.ssec.mcidasv.servermanager;
030
031import static java.util.Objects.requireNonNull;
032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
033import static edu.wisc.ssec.mcidasv.util.Contract.checkArg;
034
035import java.io.IOException;
036
037import java.net.InetSocketAddress;
038import java.net.Socket;
039import java.net.UnknownHostException;
040
041import java.util.Collections;
042import java.util.EnumMap;
043import java.util.List;
044import java.util.Map;
045import java.util.Set;
046
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import edu.wisc.ssec.mcidas.adde.AddeServerInfo;
051import edu.wisc.ssec.mcidas.adde.AddeTextReader;
052import edu.wisc.ssec.mcidas.adde.AddeURLException;
053import edu.wisc.ssec.mcidas.adde.DataSetInfo;
054
055import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
056import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
057import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
058import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
059import edu.wisc.ssec.mcidasv.servermanager.RemoteEntryEditor.AddeStatus;
060
061public class RemoteAddeEntry implements AddeEntry {
062
063    /** Typical logger object. */
064    private static final Logger logger =
065        LoggerFactory.getLogger(RemoteAddeEntry.class);
066
067    /** Represents an invalid remote ADDE entry. */
068    public static final RemoteAddeEntry INVALID_ENTRY = 
069        new Builder("localhost", "BIGBAD").invalidate().build();
070
071    /** Represents a collection of invalid remote ADDE entries. */
072    public static final List<RemoteAddeEntry> INVALID_ENTRIES = 
073        Collections.singletonList(INVALID_ENTRY);
074
075    /** Default port for remote ADDE servers. */
076    public static final int ADDE_PORT = 112;
077
078    /** 
079     * {@link String#format(String, Object...)}-friendly string for building a
080     * request to read a server's {@literal "PUBLIC.SRV"}.
081     */
082    private static final String publicSrvFormat = "adde://%s/text?compress=gzip&port=112&debug=%s&version=1&user=%s&proj=%s&file=PUBLIC.SRV";
083
084    /** Holds the accounting information for this entry. */
085    private final AddeAccount account;
086
087    /** The server {@literal "address"} of this entry. */
088    private final String address;
089
090    /** The {@literal "dataset"} of this entry. */
091    private final String group;
092
093    /** Whether or not this entry will persist between McIDAS-V sessions. */
094    private final boolean isTemporary;
095
096    /** This entry's type. */
097    private EntryType entryType;
098
099    /** Whether or not this entry is valid. */
100    private EntryValidity entryValidity;
101
102    /** Where this entry came from. */
103    private EntrySource entrySource;
104
105    /** Whether or not this entry is in the {@literal "active set"}. */
106    private EntryStatus entryStatus;
107
108    /** Allows the user to refer to this entry with an arbitrary name. */
109    private String entryAlias;
110
111    private String asStringId;
112
113    /** 
114     * Used so that the hashCode of this entry is not needlessly 
115     * recalculated.
116     * 
117     * @see #hashCode()
118     */
119    private volatile int hashCode = 0;
120
121    /**
122     * Creates a new ADDE entry using a give {@literal "ADDE entry builder"}.
123     * 
124     * @param builder Object used to build this entry.
125     */
126    private RemoteAddeEntry(Builder builder) {
127        this.account = builder.account;
128        this.address = builder.address;
129        this.group = builder.group;
130        this.entryType = builder.entryType;
131        this.entryValidity = builder.entryValidity;
132        this.entrySource = builder.entrySource;
133        this.entryStatus = builder.entryStatus;
134        this.isTemporary = builder.temporary;
135        this.entryAlias = builder.alias;
136    }
137
138    /**
139     * @return {@link #address}
140     */
141    @Override public String getAddress() {
142        return address;
143    }
144
145    /**
146     * @return {@link #group}
147     */
148    @Override public String getGroup() {
149        return group;
150    }
151
152    @Override public String getName() {
153        return "$";
154    }
155
156    /**
157     * @return {@link #account}
158     */
159    @Override public AddeAccount getAccount() {
160        return account;
161    }
162
163    /**
164     * @return {@link #entryType}
165     */
166    @Override public EntryType getEntryType() {
167        return entryType;
168    }
169
170    /**
171     * @return {@link #entryValidity}
172     */
173    @Override public EntryValidity getEntryValidity() {
174        return entryValidity;
175    }
176
177    public void setEntryValidity(final EntryValidity entryValidity) {
178        this.entryValidity = entryValidity;
179    }
180
181    /**
182     * @return {@link #entrySource}
183     */
184    @Override public EntrySource getEntrySource() {
185        return entrySource;
186    }
187
188    /**
189     * @return {@link #entryStatus}
190     */
191    @Override public EntryStatus getEntryStatus() {
192        return entryStatus;
193    }
194
195    @Override public void setEntryStatus(EntryStatus newStatus) {
196        entryStatus = newStatus;
197    }
198
199    @Override public String getEntryAlias() {
200        return entryAlias;
201    }
202
203    @Override public void setEntryAlias(final String newAlias) {
204        if (newAlias == null) {
205            throw new NullPointerException("Null aliases are not allowable.");
206        }
207        entryAlias = newAlias;
208    }
209
210    @Override public boolean isEntryTemporary() {
211        return isTemporary;
212    }
213
214    /**
215     * Handy {@code String} representation of this ADDE entry. Currently looks
216     * like {@code ADDRESS/GROUP}, but this is subject to change.
217     * 
218     * @return Alternate {@code String} representation of this entry.
219     */
220    @Override public String getEntryText() {
221        return address+'/'+group;
222    }
223
224    /**
225     * Determines whether or not the given object is equivalent to this ADDE 
226     * entry.
227     * 
228     * @param obj Object to test against. {@code null} values are okay, but 
229     * return {@code false}.
230     * 
231     * @return {@code true} if the given object is the same as this ADDE 
232     * entry, {@code false} otherwise... including when {@code o} is 
233     * {@code null}.
234     */
235    @Override public boolean equals(Object obj) {
236        if (this == obj) {
237            return true;
238        }
239        if (obj == null) {
240            return false;
241        }
242        if (!(obj instanceof RemoteAddeEntry)) {
243            return false;
244        }
245        RemoteAddeEntry other = (RemoteAddeEntry) obj;
246        if (account == null) {
247            if (other.account != null) {
248                return false;
249            }
250        } else if (!account.equals(other.account)) {
251            return false;
252        }
253        if (address == null) {
254            if (other.address != null) {
255                return false;
256            }
257        } else if (!address.equals(other.address)) {
258            return false;
259        }
260        if (entryType == null) {
261            if (other.entryType != null) {
262                return false;
263            }
264        } else if (!entryType.equals(other.entryType)) {
265            return false;
266        }
267        if (group == null) {
268            if (other.group != null) {
269                return false;
270            }
271        } else if (!group.equals(other.group)) {
272            return false;
273        }
274        if (entryAlias == null) {
275            if (other.entryAlias != null) {
276                return false;
277            }
278        } else if (!entryAlias.equals(other.entryAlias)) {
279            return false;
280        }
281        if (isTemporary != other.isTemporary) {
282            return false;
283        }
284        return true;
285    }
286
287    /**
288     * Returns a hash code for this ADDE entry. The hash code is computed 
289     * using the values of the following fields: 
290     * {@link #address}, {@link #group}, {@link #entryType}, {@link #account}.
291     * 
292     * @return Hash code value for this object.
293     */
294    @Override public int hashCode() {
295        final int prime = 31;
296        int result = 1;
297        result = prime * result + ((account == null) ? 0 : account.hashCode());
298        result = prime * result + ((address == null) ? 0 : address.hashCode());
299        result = prime * result + ((entryType == null) ? 0 : entryType.hashCode());
300        result = prime * result + ((group == null) ? 0 : group.hashCode());
301        result = prime * result + ((entryAlias == null) ? 0 : entryAlias.hashCode());
302        result = prime * result + (isTemporary ? 1231 : 1237);
303        return result;
304    }
305
306    @Override public String asStringId() {
307        if (asStringId == null) {
308            asStringId = address+'!'+group+'!'+entryType.name();
309        }
310        return asStringId;
311    }
312
313    public String toString() {
314        return String.format("[RemoteAddeEntry@%x: address=%s, group=%s, entryType=%s, entryValidity=%s, account=%s, status=%s, source=%s, temporary=%s, alias=%s]", hashCode(), address, group, entryType, entryValidity, account, entryStatus.name(), entrySource, isTemporary, entryAlias);
315    }
316
317    /**
318     * Something of a hack... this approach allows us to build a 
319     * {@code RemoteAddeEntry} in a <b>readable</b> way, despite there being
320     * multiple {@code final} fields. 
321     * 
322     * <p>The only <i>required</i> parameters are
323     * the {@link RemoteAddeEntry#address} and {@link RemoteAddeEntry#group}.</p>
324     * 
325     * <p>Some examples:</p>
326     *
327     * <pre>
328     * RemoteAddeEntry e = RemoteAddeEntry.Builder("adde.cool.com", "RTIMAGES").build();
329     * e = RemoteAddeEntry.Builder("adde.cool.com", "RTIMAGES").type(EntryType.IMAGE).account("user", "1337").build();
330     * e = RemoteAddeEntry.Builder("adde.cool.com", "RTIMAGES").account("user", "1337").type(EntryType.IMAGE).build()
331     * e = RemoteAddeEntry.Builder("a.c.com", "RTIMGS").validity(EntryValidity.VERIFIED).build();
332     * </pre>
333     */
334    public static class Builder {
335
336        /** Hostname or IP of the resulting entry. */
337        private final String address;
338
339        /** ADDE group to use for the resulting entry. */
340        private final String group;
341
342        /** 
343         * Optional {@link EntryType} of the entry. Defaults to 
344         * {@link EntryType#UNKNOWN}. 
345         */
346        private EntryType entryType = EntryType.UNKNOWN;
347
348        /** Optional {@link EntryValidity} of the entry. Defaults to 
349         * {@link EntryValidity#UNVERIFIED}. 
350         */
351        private EntryValidity entryValidity = EntryValidity.UNVERIFIED;
352
353        /** 
354         * Optional {@link EntrySource} of the entry. Defaults to 
355         * {@link EntrySource#SYSTEM}. 
356         */
357        private EntrySource entrySource = EntrySource.SYSTEM;
358
359        /** 
360         * Optional {@link EntryStatus} of the entry. Defaults to 
361         * {@link EntryStatus#ENABLED}. 
362         */
363        private EntryStatus entryStatus = EntryStatus.ENABLED;
364
365        /** 
366         * Optional {@link AddeAccount} of the entry. Defaults to 
367         * {@link RemoteAddeEntry#DEFAULT_ACCOUNT}. 
368         */
369        private AddeAccount account = RemoteAddeEntry.DEFAULT_ACCOUNT;
370
371        /** Optional description of the entry. Defaults to {@literal ""}. */
372        private String description = "";
373
374        /**
375         * Optional flag for whether or not the entry is temporary.
376         * Defaults to {@code false}.
377         */
378        private boolean temporary = false;
379
380        /** Optional alias for the entry. Default to {@literal ""}. */
381        private String alias = "";
382
383        /**
384         * Creates a new {@literal "builder"} for an ADDE entry. Note that
385         * the two parameters to this constructor are the only <i>required</i>
386         * parameters to create an ADDE entry.
387         * 
388         * @param address Address of the ADDE entry. Cannot be null.
389         * @param group Group of the ADDE entry. Cannot be null.
390         * 
391         * @throws NullPointerException if either {@code address} or 
392         * {@code group} is {@code null}.
393         */
394        public Builder(final String address, final String group) {
395            if (address == null) {
396                throw new NullPointerException("ADDE address cannot be null");
397            }
398            if (group == null) {
399                throw new NullPointerException("ADDE group cannot be null");
400            }
401
402            this.address = address.toLowerCase();
403            this.group = group;
404        }
405
406        /** 
407         * Optional {@literal "parameter"} for an ADDE entry. Allows you to
408         * specify the accounting information. If this method is not called,
409         * the resulting ADDE entry will be built with 
410         * {@link RemoteAddeEntry#DEFAULT_ACCOUNT}.
411         * 
412         * @param username Username of the ADDE account. Cannot be 
413         * {@code null}.
414         * @param project Project number for the ADDE account. Cannot be 
415         * {@code null}.
416         * 
417         * @return Current {@literal "builder"} for an ADDE entry.
418         * 
419         * @see AddeAccount#AddeAccount(String, String)
420         */
421        public Builder account(final String username, final String project) {
422            account = new AddeAccount(username, project);
423            return this;
424        }
425
426        /**
427         * Optional {@literal "parameter"} for an ADDE entry. Allows you to
428         * set the {@link RemoteAddeEntry#entryType}. If this method is not 
429         * called, {@code entryType} will default to {@link EntryType#UNKNOWN}.
430         * 
431         * @param entryType ADDE entry {@literal "type"}.
432         * 
433         * @return Current {@literal "builder"} for an ADDE entry.
434         */
435        public Builder type(EntryType entryType) {
436            this.entryType = entryType;
437            return this;
438        }
439
440        /**
441         * Optional {@literal "parameter"} for an ADDE entry. Allows you to
442         * set the {@link RemoteAddeEntry#entryValidity}. If this method is 
443         * not called, {@code entryValidity} will default to 
444         * {@link EntryValidity#UNVERIFIED}.
445         * 
446         * @param entryValidity ADDE entry {@literal "validity"}.
447         * 
448         * @return Current {@literal "builder"} for an ADDE entry.
449         */
450        public Builder validity(EntryValidity entryValidity) {
451            this.entryValidity = entryValidity;
452            return this;
453        }
454
455        /**
456         * Optional {@literal "parameter"} for an ADDE entry. Allows you to
457         * set the {@link RemoteAddeEntry#entrySource}. If this method is not 
458         * called, {@code entrySource} will default to 
459         * {@link EntrySource#SYSTEM}.
460         * 
461         * @param entrySource ADDE entry {@literal "source"}.
462         * 
463         * @return Current {@literal "builder"} for an ADDE entry.
464         */
465        public Builder source(EntrySource entrySource) {
466            this.entrySource = entrySource;
467            return this;
468        }
469
470        /**
471         * Optional {@literal "parameter"} for an ADDE entry. Allows you to
472         * set the {@link RemoteAddeEntry#entryStatus}. If this method is not 
473         * called, {@code entryStatus} will default to 
474         * {@link EntryStatus#ENABLED}.
475         * 
476         * @param entryStatus ADDE entry {@literal "status"}.
477         * 
478         * @return Current {@literal "builder"} for an ADDE entry.
479         */
480        public Builder status(EntryStatus entryStatus) {
481            this.entryStatus = entryStatus;
482            return this;
483        }
484
485        /**
486         * Convenient way to generate a new, invalid entry.
487         * 
488         * @return Current {@literal "builder"} for an ADDE entry.
489         */
490        public Builder invalidate() {
491            this.entryType = EntryType.INVALID;
492            this.entryValidity = EntryValidity.INVALID;
493            this.entrySource = EntrySource.INVALID;
494            this.entryStatus = EntryStatus.INVALID;
495            return this;
496        }
497
498        /**
499         * Optionally control whether or not the resulting entry is
500         * {@literal "temporary"}.
501         * 
502         * @param temporary Whether or not the entry is temporary.
503         * 
504         * @return Current {@literal "builder"} for an ADDE entry.
505         */
506        public Builder temporary(boolean temporary) {
507            this.temporary = temporary;
508            return this;
509        }
510
511        /**
512         * Optionally sets the {@literal "alias"} that can be used to refer to
513         * the resulting entry.
514         * 
515         * @param alias Alias for the resulting entry.
516         * 
517         * @return Current {@literal "builder"} for an ADDE entry.
518         */
519        public Builder alias(final String alias) {
520            this.alias = alias;
521            return this;
522        }
523
524        /** 
525         * Creates an entry based upon the values supplied to the other 
526         * methods. 
527         * 
528         * @return A newly created {@code RemoteAddeEntry}.
529         */
530        public RemoteAddeEntry build() {
531            return new RemoteAddeEntry(this);
532        }
533    }
534
535    /**
536     * Tries to connect to a given {@code RemoteAddeEntry} and read the list
537     * of ADDE {@literal "groups"} available to the public.
538     * 
539     * @param entry The {@code RemoteAddeEntry} to query. Cannot be {@code null}.
540     * 
541     * @return {@link Set} of public groups on {@code entry}.
542     * 
543     * @throws NullPointerException if {@code entry} is {@code null}.
544     * @throws IllegalArgumentException if the server address is an empty 
545     * {@link String}.
546     */
547    public static Set<String> readPublicGroups(final RemoteAddeEntry entry) {
548        requireNonNull(entry, "entry cannot be null");
549        requireNonNull(entry.getAddress());
550        checkArg(!entry.getAddress().isEmpty());
551
552        String user = entry.getAccount().getUsername();
553        if ((user == null) || user.isEmpty()) {
554            user = RemoteAddeEntry.DEFAULT_ACCOUNT.getUsername();
555        }
556
557        String proj = entry.getAccount().getProject();
558        if ((proj == null) || proj.isEmpty()) {
559            proj = RemoteAddeEntry.DEFAULT_ACCOUNT.getProject();
560        }
561
562        boolean debugUrl = EntryStore.isAddeDebugEnabled(false);
563        String url = String.format(publicSrvFormat, entry.getAddress(), debugUrl, user, proj);
564
565        Set<String> groups = newLinkedHashSet();
566
567        AddeTextReader reader = new AddeTextReader(url);
568        if ("OK".equals(reader.getStatus())) {
569            for (String line : (List<String>)reader.getLinesOfText()) {
570                String[] pairs = line.trim().split(",");
571                for (String pair : pairs) {
572                    if ((pair == null) || pair.isEmpty() || !pair.startsWith("N1")) {
573                        continue;
574                    }
575                    String[] keyval = pair.split("=");
576                    if ((keyval.length != 2) || keyval[0].isEmpty() || keyval[1].isEmpty() || !keyval[0].equals("N1")) {
577                        continue;
578                    }
579                    groups.add(keyval[1]);
580                }
581            }
582        }
583        return groups;
584    }
585
586    /**
587     * Determines whether or not the server specified in {@code entry} is
588     * listening on port 112.
589     * 
590     * @param entry Descriptor containing the server to check.
591     * 
592     * @return {@code true} if a connection was opened, {@code false} otherwise.
593     * 
594     * @throws NullPointerException if {@code entry} is null.
595     */
596    public static boolean checkHost(final RemoteAddeEntry entry) {
597        requireNonNull(entry, "entry cannot be null");
598        String host = entry.getAddress();
599        boolean connected;
600        if (host.startsWith("localhost:")) {
601            connected = true;
602        } else {
603            Socket socket = new Socket();
604            try {
605                socket.connect(new InetSocketAddress(host, ADDE_PORT), 10);
606                connected = true;
607                socket.close();
608            } catch (UnknownHostException e) {
609                logger.debug("can't resolve IP for '{}'", host);
610                connected = false;
611            } catch (IOException e) {
612                logger.debug("IO problem while connecting to '{}': {}", entry.getAddress(), e.getMessage());
613                connected = false;
614            }
615        }
616        logger.trace("host={} result={}", entry.getAddress(), connected);
617        return connected;
618    }
619
620    /**
621     * Attempts to verify whether or not the information in a given
622     * RemoteAddeEntry represents a valid remote ADDE server. If not, the
623     * method tries to determine which parts of the entry are invalid.
624     * 
625     * <p>Note that this method uses {@code checkHost(RemoteAddeEntry)} to 
626     * verify that the server is listening. To forego the check, simply call
627     * {@code checkEntry(false, entry)}.
628     * 
629     * @param entry {@code RemoteAddeEntry} to check. Cannot be 
630     * {@code null}.
631     * 
632     * @return The {@link AddeStatus} that represents the verification status
633     * of {@code entry}.
634     * 
635     * @see #checkHost(RemoteAddeEntry)
636     * @see #checkEntry(boolean, RemoteAddeEntry)
637     */
638    public static AddeStatus checkEntry(final RemoteAddeEntry entry) {
639        return checkEntry(true, entry);
640    }
641
642    /**
643     * Attempts to verify whether or not the information in a given 
644     * RemoteAddeEntry represents a valid remote ADDE server. If not, the
645     * method tries to determine which parts of the entry are invalid.
646     * 
647     * @param checkHost {@code true} tries to connect to the remote ADDE server
648     * before doing anything else.
649     * @param entry {@code RemoteAddeEntry} to check. Cannot be 
650     * {@code null}.
651     * 
652     * @return The {@link AddeStatus} that represents the verification status
653     * of {@code entry}.
654     * 
655     * @throws NullPointerException if {@code entry} is {@code null}.
656     * 
657     * @see AddeStatus
658     */
659    public static AddeStatus checkEntry(final boolean checkHost, final RemoteAddeEntry entry) {
660        requireNonNull(entry, "Cannot check a null entry");
661
662        if (checkHost && !checkHost(entry)) {
663            return AddeStatus.BAD_SERVER;
664        }
665
666        String server = entry.getAddress();
667        String type = entry.getEntryType().toString();
668        String username = entry.getAccount().getUsername();
669        String project = entry.getAccount().getProject();
670        String[] servers = { server };
671        AddeServerInfo serverInfo = new AddeServerInfo(servers);
672
673        // I just want to go on the record here: 
674        // AddeServerInfo#setUserIDAndProjString(String) was not a good API 
675        // decision.
676        serverInfo.setUserIDandProjString("user="+username+"&proj="+project);
677        int status = serverInfo.setSelectedServer(server, type);
678        if (status == -2) {
679            return AddeStatus.NO_METADATA;
680        }
681        if (status == -1) {
682            return AddeStatus.BAD_ACCOUNTING;
683        }
684
685        serverInfo.setSelectedGroup(entry.getGroup());
686        String[] datasets = serverInfo.getDatasetList();
687        if ((datasets != null) && (datasets.length > 0)) {
688            // TJJ 7 Nov 2013, not my proudest moment. See Inq #905
689            // if type is NEXR, this is a Radar server, not Image
690            String ff = serverInfo.getFileFormat();
691            if ("NEXR".equals(ff)) {
692                entry.entryType = AddeEntry.EntryType.RADAR;
693            }
694            return AddeStatus.OK;
695        }
696        // TJJ - see Inq 1975, needed to add this hack because it seems
697        // imagery always technically validates as radar.
698        else if (!"RADAR".equals(type)) {
699            // try dsinfo
700            String addeUrl = "adde://"+server+"/datasetinfo?group="+entry.getGroup()+"&type="+type+"&user="+username+"&proj="+project+"&compress=gzip&port=112&debug=true&version=1";
701            logger.trace("dsinfo url: '{}'", addeUrl);
702            try {
703                DataSetInfo dsinfo = new DataSetInfo(addeUrl);
704                Map<?, ?> descriptionTable = dsinfo.getDescriptionTable();
705                if ((descriptionTable != null) && !descriptionTable.isEmpty()) {
706                    return AddeStatus.OK;
707                }
708            } catch (AddeURLException e) {
709                logger.trace("dsinfo failed for url: '{}'", addeUrl);
710            }
711            return AddeStatus.BAD_GROUP;
712        }
713        // at this point can only be a bad group
714        else {
715            return AddeStatus.BAD_GROUP;
716        }
717    }
718
719    /**
720     * Determine the types of ADDE data within the given {@code group} on
721     * {@code host}. This method uses the {@literal "default"} ADDE user name
722     * and project number.
723     *
724     * <p>Note: <b>parameters cannot be {@code null}.</b></p>
725     *
726     * @param host Host to check.
727     * @param group ADDE group.
728     *
729     * @return {@link EnumMap} that maps ADDE data type to whether or not it
730     * is available for the given {@code host} and {@code group}.
731     */
732    public static Map<EntryType, AddeStatus> checkEntryTypes(final String host, final String group) {
733        return checkEntryTypes(host, group, AddeEntry.DEFAULT_ACCOUNT.getUsername(), AddeEntry.DEFAULT_ACCOUNT.getProject());
734    }
735
736    /**
737     * Determine the types of ADDE data within the given {@code group} on
738     * {@code host}.
739     *
740     * <p>Note: <b>parameters cannot be {@code null}.</b></p>
741     *
742     * @param host Host to check.
743     * @param group ADDE group.
744     * @param user ADDE user name.
745     * @param proj ADDE project number.
746     *
747     * @return {@link EnumMap} that maps ADDE data type to whether or not it
748     * is available for the given set of parameters.
749     *
750     * @see #checkEntry(boolean, RemoteAddeEntry)
751     */
752    public static Map<EntryType, AddeStatus> checkEntryTypes(final String host, final String group, final String user, final String proj) {
753        // current type count is six. doubling it to be safe.
754        Map<EntryType, AddeStatus> valid = new EnumMap<>(EntryType.class);
755        RemoteAddeEntry entry = new Builder(host, group).account(user, proj).build();
756        for (RemoteAddeEntry tmp : EntryTransforms.createEntriesFrom(entry)) {
757            valid.put(tmp.entryType, checkEntry(true, tmp));
758        }
759        return valid;
760    }
761
762    /**
763     * Attempts to determine the {@literal "public"} ADDE groups available on
764     * the given {@code host}.
765     *
766     * <p>Note: this method uses the {@literal "default"} ADDE user name and
767     * project number.</p>
768     *
769     *
770     * @param host Host from which public groups are to be read. Cannot be {@code null}.
771     *
772     * @return {@link Set} of the public groups on {@code host}. The
773     * {@code Set} will be empty if there are no groups.
774     */
775    public static Set<String> readPublicGroups(final String host) {
776        return readGroups(host, AddeEntry.DEFAULT_ACCOUNT.getUsername(), AddeEntry.DEFAULT_ACCOUNT.getProject());
777    }
778
779    /**
780     * Attempts to determine which (if any) ADDE groups are available on the
781     * given {@code host}.
782     *
783     * <p>Note: <b>parameters cannot be {@code null}.</b></p>
784     *
785     * @param host Host from which public groups are to be read.
786     * @param user ADDE user name.
787     * @param proj ADDE project number.
788     *
789     * @return {@link Set} of the groups on {@code host}. The {@code Set} will
790     * be empty if there are no groups.
791     */
792    public static Set<String> readGroups(final String host, final String user, final String proj) {
793        RemoteAddeEntry entry = new Builder(host, "").account(user, proj).build();
794        return readPublicGroups(entry);
795    }
796}