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 }