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