001/*
002 * $Id: RemoteAddeEntry.java,v 1.20 2011/03/24 16:06:34 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
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
031package edu.wisc.ssec.mcidasv.servermanager;
032
033import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
034import static edu.wisc.ssec.mcidasv.util.Contract.checkArg;
035import static edu.wisc.ssec.mcidasv.util.Contract.notNull;
036
037import java.io.IOException;
038import java.net.Socket;
039import java.net.UnknownHostException;
040import java.util.Collections;
041import java.util.LinkedHashMap;
042import java.util.LinkedHashSet;
043import java.util.List;
044import java.util.Map;
045import java.util.Set;
046
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import edu.wisc.ssec.mcidas.adde.AddeServerInfo;
051import edu.wisc.ssec.mcidas.adde.AddeTextReader;
052
053import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
054import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
055import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
056import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
057import edu.wisc.ssec.mcidasv.servermanager.RemoteEntryEditor.AddeStatus;
058
059public 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}