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 }