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