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}