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