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 }