001    /*
002     * $Id: RemoteAddeEntry.java,v 1.21 2012/02/19 17:35:48 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    
031    package edu.wisc.ssec.mcidasv.servermanager;
032    
033    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
034    import static edu.wisc.ssec.mcidasv.util.Contract.checkArg;
035    import static edu.wisc.ssec.mcidasv.util.Contract.notNull;
036    
037    import java.io.IOException;
038    import java.net.Socket;
039    import java.net.UnknownHostException;
040    import java.util.Collections;
041    import java.util.LinkedHashMap;
042    import java.util.LinkedHashSet;
043    import java.util.List;
044    import java.util.Map;
045    import java.util.Set;
046    
047    import org.slf4j.Logger;
048    import org.slf4j.LoggerFactory;
049    
050    import edu.wisc.ssec.mcidas.adde.AddeServerInfo;
051    import edu.wisc.ssec.mcidas.adde.AddeTextReader;
052    
053    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
054    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
055    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
056    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
057    import edu.wisc.ssec.mcidasv.servermanager.RemoteEntryEditor.AddeStatus;
058    
059    public class RemoteAddeEntry implements AddeEntry {
060    
061        /** Typical logger object. */
062        private static final Logger logger = 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 java.lang.String#format(String, Object...)}-friendly string for 
077         * building a request to read a server's 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    //    /** Err... */
091    //    // TODO(jon): wait, what is this?
092    //    private final String description;
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        }
133    
134        /**
135         * @return {@link #address}
136         */
137        public String getAddress() {
138            return address;
139        }
140    
141        /**
142         * @return {@link #group}
143         */
144        public String getGroup() {
145            return group;
146        }
147    
148        public String getName() {
149            return "$";
150        }
151    
152        /**
153         * @return {@link #account}
154         */
155        public AddeAccount getAccount() {
156            return account;
157        }
158    
159        /**
160         * @return {@link #entryType}
161         */
162        public EntryType getEntryType() {
163            return entryType;
164        }
165    
166        /**
167         * @return {@link #entryValidity}
168         */
169        public EntryValidity getEntryValidity() {
170            return entryValidity;
171        }
172    
173        public void setEntryValidity(final EntryValidity entryValidity) {
174            this.entryValidity = entryValidity;
175        }
176    
177        /**
178         * @return {@link #entrySource}
179         */
180        public EntrySource getEntrySource() {
181            return entrySource;
182        }
183    
184        /**
185         * @return {@link #entryStatus}
186         */
187        public EntryStatus getEntryStatus() {
188            return entryStatus;
189        }
190    
191        public void setEntryStatus(EntryStatus newStatus) {
192            entryStatus = newStatus;
193        }
194    
195        public String getEntryAlias() {
196            return entryAlias;
197        }
198    
199        public void setEntryAlias(final String newAlias) {
200            if (newAlias == null) {
201                throw new NullPointerException("Null aliases are not allowable.");
202            }
203            entryAlias = newAlias;
204        }
205    
206        /**
207         * Handy {@code String} representation of this ADDE entry. Currently looks
208         * like {@code ADDRESS/GROUP}, but this is subject to change.
209         * 
210         * @return Alternate {@code String} representation of this entry.
211         */
212        public String getEntryText() {
213            return address+'/'+group;
214        }
215    
216        /**
217         * Determines whether or not the given object is equivalent to this ADDE 
218         * entry.
219         * 
220         * @param obj Object to test against. {@code null} values are okay, but 
221         * return {@code false}.
222         * 
223         * @return {@code true} if the given object is the same as this ADDE 
224         * entry, {@code false} otherwise... including when {@code o} is 
225         * {@code null}.
226         */
227        @Override public boolean equals(Object obj) {
228            if (this == obj) {
229                return true;
230            }
231            if (obj == null) {
232                return false;
233            }
234            if (!(obj instanceof RemoteAddeEntry)) {
235                return false;
236            }
237            RemoteAddeEntry other = (RemoteAddeEntry) obj;
238            if (account == null) {
239                if (other.account != null) {
240                    return false;
241                }
242            } else if (!account.equals(other.account)) {
243                return false;
244            }
245            if (address == null) {
246                if (other.address != null) {
247                    return false;
248                }
249            } else if (!address.equals(other.address)) {
250                return false;
251            }
252            if (entryType == null) {
253                if (other.entryType != null) {
254                    return false;
255                }
256            } else if (!entryType.equals(other.entryType)) {
257                return false;
258            }
259            if (group == null) {
260                if (other.group != null) {
261                    return false;
262                }
263            } else if (!group.equals(other.group)) {
264                return false;
265            }
266            return true;
267        }
268    
269        /**
270         * Returns a hash code for this ADDE entry. The hash code is computed 
271         * using the values of the following fields: 
272         * {@link #address}, {@link #group}, {@link #entryType}, {@link #account}.
273         * 
274         * @return Hash code value for this object.
275         */
276        @Override public int hashCode() {
277            final int prime = 31;
278            int result = 1;
279            result = prime * result + ((account == null) ? 0 : account.hashCode());
280            result = prime * result + ((address == null) ? 0 : address.hashCode());
281            result = prime * result + ((entryType == null) ? 0 : entryType.hashCode());
282            result = prime * result + ((group == null) ? 0 : group.hashCode());
283            return result;
284        }
285    
286        public String asStringId() {
287            if (asStringId == null) {
288                asStringId = address+'!'+group+'!'+entryType.name();
289            }
290            return asStringId;
291        }
292    
293        public String toString() {
294            return String.format("[RemoteAddeEntry@%x: address=%s, group=%s, entryType=%s, entryValidity=%s, account=%s, status=%s, source=%s]", hashCode(), address, group, entryType, entryValidity, account, entryStatus.name(), entrySource);
295        }
296    
297        /**
298         * Something of a hack... this approach allows us to build a 
299         * {@code RemoteAddeEntry} in a <b>readable</b> way, despite there being
300         * multiple {@code final} fields. 
301         * 
302         * <p>The only <i>required</i> parameters are
303         * the {@link RemoteAddeEntry#address} and {@link RemoteAddeEntry#group}.
304         * 
305         * <p>Some examples:<br/>
306         * <pre>
307         * RemoteAddeEntry e = RemoteAddeEntry.Builder("adde.cool.com", "RTIMAGES").build();
308         * e = RemoteAddeEntry.Builder("adde.cool.com", "RTIMAGES").type(EntryType.IMAGE).account("user", "1337").build();
309         * e = RemoteAddeEntry.Builder("adde.cool.com", "RTIMAGES").account("user", "1337").type(EntryType.IMAGE).build()
310         * e = RemoteAddeEntry.Builder("a.c.com", "RTIMGS").validity(EntryValidity.VERIFIED).build();
311         * </pre>
312         * 
313         */
314        public static class Builder {
315            private final String address;
316            private final String group;
317    
318            /** 
319             * Optional {@link EntryType} of the entry. Defaults to 
320             * {@link EntryType#UNKNOWN}. 
321             */
322            private EntryType entryType = EntryType.UNKNOWN;
323    
324            /** Optional {@link EntryValidity} of the entry. Defaults to 
325             * {@link EntryValidity#UNVERIFIED}. 
326             */
327            private EntryValidity entryValidity = EntryValidity.UNVERIFIED;
328    
329            /** 
330             * Optional {@link EntrySource} of the entry. Defaults to 
331             * {@link EntrySource#SYSTEM}. 
332             */
333            private EntrySource entrySource = EntrySource.SYSTEM;
334    
335            /** 
336             * Optional {@link EntryStatus} of the entry. Defaults to 
337             * {@link EntryStatus#ENABLED}. 
338             */
339            private EntryStatus entryStatus = EntryStatus.ENABLED;
340    
341            /** 
342             * Optional {@link AddeAccount} of the entry. Defaults to 
343             * {@link RemoteAddeEntry#DEFAULT_ACCOUNT}. 
344             */
345            private AddeAccount account = RemoteAddeEntry.DEFAULT_ACCOUNT;
346    
347            /** Optional description of the entry. Defaults to {@literal ""}. */
348            private String description = "";
349    
350            /**
351             * Creates a new {@literal "builder"} for an ADDE entry. Note that
352             * the two parameters to this constructor are the only <i>required</i>
353             * parameters to create an ADDE entry.
354             * 
355             * @param address Address of the ADDE entry. Cannot be null.
356             * @param group Group of the ADDE entry. Cannot be null.
357             * 
358             * @throws NullPointerException if either {@code address} or 
359             * {@code group} is {@code null}.
360             */
361            public Builder(final String address, final String group) {
362                if (address == null) {
363                    throw new NullPointerException("ADDE address cannot be null");
364                }
365                if (group == null) {
366                    throw new NullPointerException("ADDE group cannot be null");
367                }
368    
369                this.address = address.toLowerCase();
370                this.group = group;
371            }
372    
373            /** 
374             * Optional {@literal "parameter"} for an ADDE entry. Allows you to
375             * specify the accounting information. If this method is not called,
376             * the resulting ADDE entry will be built with 
377             * {@link RemoteAddeEntry#DEFAULT_ACCOUNT}.
378             * 
379             * @param username Username of the ADDE account. Cannot be 
380             * {@code null}.
381             * @param project Project number for the ADDE account. Cannot be 
382             * {@code null}.
383             * 
384             * @return Current {@literal "builder"} for an ADDE entry.
385             * 
386             * @see AddeAccount#AddeAccount(String, String)
387             */
388            public Builder account(final String username, final String project) {
389                account = new AddeAccount(username, project);
390                return this;
391            }
392    
393            /**
394             * Optional {@literal "parameter"} for an ADDE entry. Allows you to
395             * set the {@link RemoteAddeEntry#entryType}. If this method is not 
396             * called, {@code entryType} will default to {@link EntryType#UNKNOWN}.
397             * 
398             * @param entryType ADDE entry {@literal "type"}.
399             * 
400             * @return Current {@literal "builder"} for an ADDE entry.
401             */
402            public Builder type(EntryType entryType) {
403                this.entryType = entryType;
404                return this;
405            }
406    
407            /**
408             * Optional {@literal "parameter"} for an ADDE entry. Allows you to
409             * set the {@link RemoteAddeEntry#entryValidity}. If this method is 
410             * not called, {@code entryValidity} will default to 
411             * {@link EntryValidity#UNVERIFIED}.
412             * 
413             * @param entryValidity ADDE entry {@literal "validity"}.
414             * 
415             * @return Current {@literal "builder"} for an ADDE entry.
416             */
417            public Builder validity(EntryValidity entryValidity) {
418                this.entryValidity = entryValidity;
419                return this;
420            }
421    
422            /**
423             * Optional {@literal "parameter"} for an ADDE entry. Allows you to
424             * set the {@link RemoteAddeEntry#entrySource}. If this method is not 
425             * called, {@code entrySource} will default to 
426             * {@link EntrySource#SYSTEM}.
427             * 
428             * @param entrySource ADDE entry {@literal "source"}.
429             * 
430             * @return Current {@literal "builder"} for an ADDE entry.
431             */
432            public Builder source(EntrySource entrySource) {
433                this.entrySource = entrySource;
434                return this;
435            }
436    
437            /**
438             * Optional {@literal "parameter"} for an ADDE entry. Allows you to
439             * set the {@link RemoteAddeEntry#entryStatus}. If this method is not 
440             * called, {@code entryStatus} will default to 
441             * {@link EntryStatus#ENABLED}.
442             * 
443             * @param entryStatus ADDE entry {@literal "status"}.
444             * 
445             * @return Current {@literal "builder"} for an ADDE entry.
446             */
447            public Builder status(EntryStatus entryStatus) {
448                this.entryStatus = entryStatus;
449                return this;
450            }
451    
452            public Builder invalidate() {
453                this.entryType = EntryType.INVALID;
454                this.entryValidity = EntryValidity.INVALID;
455                this.entrySource = EntrySource.INVALID;
456                this.entryStatus = EntryStatus.INVALID;
457                return this;
458            }
459    
460            /** 
461             * Creates an entry based upon the values supplied to the other 
462             * methods. 
463             */
464            public RemoteAddeEntry build() {
465                return new RemoteAddeEntry(this);
466            }
467        }
468    
469        /**
470         * Tries to connect to a given {@link RemoteAddeEntry} and read the list
471         * of ADDE {@literal "groups"} available to the public.
472         * 
473         * @param entry The {@code RemoteAddeEntry} to query. Cannot be {@code null}.
474         * 
475         * @return The {@link Set} of public groups on {@code entry}.
476         * 
477         * @throws NullPointerException if {@code entry} is {@code null}.
478         * @throws IllegalArgumentException if the server address is an empty 
479         * {@link String}.
480         */
481        public static Set<String> readPublicGroups(final RemoteAddeEntry entry) {
482            notNull(entry, "entry cannot be null");
483            notNull(entry.getAddress());
484            checkArg((entry.getAddress().length() != 0));
485    
486            String user = entry.getAccount().getUsername();
487            if (user == null || user.length() == 0) {
488                user = RemoteAddeEntry.DEFAULT_ACCOUNT.getUsername();
489            }
490    
491            String proj = entry.getAccount().getProject();
492            if (proj == null || proj.length() == 0) {
493                proj = RemoteAddeEntry.DEFAULT_ACCOUNT.getProject();
494            }
495    
496            boolean debugUrl = EntryStore.isAddeDebugEnabled(false);
497            String url = String.format(publicSrvFormat, entry.getAddress(), debugUrl, user, proj);
498    
499            Set<String> groups = newLinkedHashSet();
500    
501            AddeTextReader reader = new AddeTextReader(url);
502            if ("OK".equals(reader.getStatus())) {
503                for (String line : (List<String>)reader.getLinesOfText()) {
504                    String[] pairs = line.trim().split(",");
505                    for (String pair : pairs) {
506                        if (pair == null || pair.length() == 0 || !pair.startsWith("N1")) {
507                            continue;
508                        }
509                        String[] keyval = pair.split("=");
510                        if (keyval.length != 2 || keyval[0].length() == 0 || keyval[1].length() == 0 || !keyval[0].equals("N1")) {
511                            continue;
512                        }
513                        groups.add(keyval[1]);
514                    }
515                }
516            }
517            return groups;
518        }
519    
520        /**
521         * Determines whether or not the server specified in {@code entry} is
522         * listening on port 112.
523         * 
524         * @param entry Descriptor containing the server to check.
525         * 
526         * @return {@code true} if a connection was opened, {@code false} otherwise.
527         * 
528         * @throws NullPointerException if {@code entry} is null.
529         */
530        public static boolean checkHost(final RemoteAddeEntry entry) {
531            notNull(entry, "entry cannot be null");
532            String host = entry.getAddress();
533            Socket socket = null;
534            boolean connected = false;
535            try { 
536                socket = new Socket(host, ADDE_PORT);
537                connected = true;
538            } catch (UnknownHostException e) {
539                logger.debug("can't resolve IP for '{}'", entry.getAddress());
540                connected = false;
541            } catch (IOException e) {
542                logger.debug("IO problem while connecting to '{}': {}", entry.getAddress(), e.getMessage());
543                connected = false;
544            }
545            try {
546                socket.close();
547            } catch (Exception e) {}
548            logger.debug("host={} result={}", entry.getAddress(), connected);
549            return connected;
550        }
551    
552        /**
553         * Attempts to verify whether or not the information in a given 
554         * {@link RemoteAddeEntry} represents a valid remote ADDE server. If not,
555         * the method tries to determine which parts of the entry are invalid.
556         * 
557         * <p>Note that this method uses {@code checkHost(RemoteAddeEntry)} to 
558         * verify that the server is listening. To forego the check, simply call
559         * {@code checkEntry(false, entry)}.
560         * 
561         * @param entry {@code RemoteAddeEntry} to check. Cannot be 
562         * {@code null}.
563         * 
564         * @return The {@link AddeStatus} that represents the verification status
565         * of {@code entry}.
566         * 
567         * @see #checkHost(RemoteAddeEntry)
568         * @see #checkEntry(boolean, RemoteAddeEntry)
569         */
570        public static AddeStatus checkEntry(final RemoteAddeEntry entry) {
571            return checkEntry(true, entry);
572        }
573    
574        /**
575         * Attempts to verify whether or not the information in a given 
576         * {@link RemoteAddeEntry} represents a valid remote ADDE server. If not,
577         * the method tries to determine which parts of the entry are invalid.
578         * 
579         * @param checkHost {@code true} tries to connect to the remote ADDE server
580         * before doing anything else.
581         * @param entry {@code RemoteAddeEntry} to check. Cannot be 
582         * {@code null}.
583         * 
584         * @return The {@link AddeStatus} that represents the verification status
585         * of {@code entry}.
586         * 
587         * @throws NullPointerException if {@code entry} is {@code null}.
588         * 
589         * @see AddeStatus
590         */
591        public static AddeStatus checkEntry(final boolean checkHost, final RemoteAddeEntry entry) {
592            notNull(entry, "Cannot check a null entry");
593    
594            if (checkHost && !checkHost(entry)) {
595                return AddeStatus.BAD_SERVER;
596            }
597    
598            String server = entry.getAddress();
599            String type = entry.getEntryType().toString();
600            String username = entry.getAccount().getUsername();
601            String project = entry.getAccount().getProject();
602            String[] servers = { server };
603            AddeServerInfo serverInfo = new AddeServerInfo(servers);
604    
605            // I just want to go on the record here: 
606            // AddeServerInfo#setUserIDAndProjString(String) was not a good API 
607            // decision.
608            serverInfo.setUserIDandProjString("user="+username+"&proj="+project);
609            int status = serverInfo.setSelectedServer(server, type);
610            if (status == -2) {
611                return AddeStatus.NO_METADATA;
612            }
613            if (status == -1) {
614                return AddeStatus.BAD_ACCOUNTING;
615            }
616    
617            serverInfo.setSelectedGroup(entry.getGroup());
618            String[] datasets = serverInfo.getDatasetList();
619            if (datasets != null && datasets.length > 0) {
620                return AddeStatus.OK;
621            } else {
622                return AddeStatus.BAD_GROUP;
623            }
624        }
625    
626        public static Map<EntryType, AddeStatus> checkEntryTypes(final String host, final String group) {
627            return checkEntryTypes(host, group, AddeEntry.DEFAULT_ACCOUNT.getUsername(), AddeEntry.DEFAULT_ACCOUNT.getProject());
628        }
629    
630        public static Map<EntryType, AddeStatus> checkEntryTypes(final String host, final String group, final String user, final String proj) {
631            Map<EntryType, AddeStatus> valid = new LinkedHashMap<EntryType, AddeStatus>();
632            RemoteAddeEntry entry = new Builder(host, group).account(user, proj).build();
633            for (RemoteAddeEntry tmp : EntryTransforms.createEntriesFrom(entry)) {
634                valid.put(tmp.getEntryType(), checkEntry(true, tmp));
635            }
636            return valid;
637        }
638    
639        public static Set<String> readPublicGroups(final String host) {
640            return readGroups(host, AddeEntry.DEFAULT_ACCOUNT.getUsername(), AddeEntry.DEFAULT_ACCOUNT.getProject());
641        }
642    
643        public static Set<String> readGroups(final String host, final String user, final String proj) {
644            RemoteAddeEntry entry = new Builder(host, "").account(user, proj).build();
645            return readPublicGroups(entry);
646        }
647    }