001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2008 Sun Microsystems, Inc. 025 * Portions Copyright 2014-2015 ForgeRock AS 026 */ 027package org.opends.guitools.controlpanel.ui; 028 029import static org.opends.messages.AdminToolMessages.*; 030 031import java.awt.Component; 032import java.awt.GridBagConstraints; 033import java.awt.event.ActionEvent; 034import java.awt.event.ActionListener; 035import java.io.ByteArrayOutputStream; 036import java.io.File; 037import java.io.FileInputStream; 038import java.util.ArrayList; 039 040import javax.swing.Box; 041import javax.swing.ButtonGroup; 042import javax.swing.Icon; 043import javax.swing.JButton; 044import javax.swing.JLabel; 045import javax.swing.JRadioButton; 046import javax.swing.JTextField; 047import javax.swing.text.JTextComponent; 048 049import org.forgerock.i18n.LocalizableMessage; 050import org.forgerock.i18n.slf4j.LocalizedLogger; 051import org.opends.guitools.controlpanel.datamodel.BinaryValue; 052import org.opends.guitools.controlpanel.event.BrowseActionListener; 053import org.opends.guitools.controlpanel.event.ConfigurationChangeEvent; 054import org.opends.guitools.controlpanel.util.BackgroundTask; 055import org.opends.guitools.controlpanel.util.Utilities; 056import org.opends.server.types.Schema; 057 058/** 059 * Panel that is displayed in the dialog where the user can specify the value 060 * of a binary attribute. 061 */ 062public class BinaryAttributeEditorPanel extends StatusGenericPanel 063{ 064 private static final long serialVersionUID = -877248486446244170L; 065 private JRadioButton useFile; 066 private JRadioButton useBase64; 067 private JTextField file; 068 private JButton browse; 069 private JLabel lFile; 070 private JTextField base64; 071 private JLabel imagePreview; 072 private JButton refreshButton; 073 private JLabel lImage = Utilities.createDefaultLabel(); 074 private JLabel attrName; 075 076 private BinaryValue value; 077 078 private boolean valueChanged; 079 080 private static final int MAX_IMAGE_HEIGHT = 300; 081 private static final int MAX_BASE64_TO_DISPLAY = 3 * 1024; 082 083 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 084 085 /** 086 * Default constructor. 087 * 088 */ 089 public BinaryAttributeEditorPanel() 090 { 091 super(); 092 createLayout(); 093 } 094 095 /** 096 * Sets the value to be displayed in the panel. 097 * @param attrName the attribute name. 098 * @param value the binary value. 099 */ 100 public void setValue(final String attrName, 101 final BinaryValue value) 102 { 103 final boolean launchBackground = this.value != value; 104// Read the file or encode the base 64 content. 105 BackgroundTask<Void> worker = new BackgroundTask<Void>() 106 { 107 /** {@inheritDoc} */ 108 @Override 109 public Void processBackgroundTask() throws Throwable 110 { 111 try 112 { 113 Thread.sleep(1000); 114 } 115 catch (Throwable t) 116 { 117 } 118 valueChanged = false; 119 BinaryAttributeEditorPanel.this.attrName.setText(attrName); 120 if (hasImageSyntax(attrName)) 121 { 122 if (value != null) 123 { 124 BinaryAttributeEditorPanel.updateImage(lImage, value.getBytes()); 125 } 126 else 127 { 128 lImage.setIcon(null); 129 lImage.setText( 130 INFO_CTRL_PANEL_NO_VALUE_SPECIFIED.get().toString()); 131 } 132 setImageVisible(true); 133 useFile.setSelected(true); 134 base64.setText(""); 135 } 136 else 137 { 138 lImage.setIcon(null); 139 lImage.setText(""); 140 setImageVisible(false); 141 142 if (value != null) 143 { 144 BinaryAttributeEditorPanel.updateBase64(base64, value.getBytes()); 145 } 146 } 147 148 if (value != null) 149 { 150 if (value.getType() == BinaryValue.Type.BASE64_STRING) 151 { 152 file.setText(""); 153 } 154 else 155 { 156 file.setText(value.getFile().getAbsolutePath()); 157 useFile.setSelected(true); 158 } 159 } 160 else 161 { 162 base64.setText(""); 163 file.setText(""); 164 useFile.setSelected(true); 165 } 166 167 BinaryAttributeEditorPanel.this.value = value; 168 169 return null; 170 } 171 172 /** {@inheritDoc} */ 173 @Override 174 public void backgroundTaskCompleted(Void returnValue, Throwable t) 175 { 176 setPrimaryValid(useFile); 177 setPrimaryValid(useBase64); 178 BinaryAttributeEditorPanel.this.attrName.setText(attrName); 179 setEnabledOK(true); 180 displayMainPanel(); 181 updateEnabling(); 182 packParentDialog(); 183 if (t != null) 184 { 185 logger.warn(LocalizableMessage.raw("Error reading binary contents: "+t, t)); 186 } 187 } 188 }; 189 if (launchBackground) 190 { 191 setEnabledOK(false); 192 displayMessage(INFO_CTRL_PANEL_READING_SUMMARY.get()); 193 worker.startBackgroundTask(); 194 } 195 else 196 { 197 setPrimaryValid(lFile); 198 setPrimaryValid(useFile); 199 setPrimaryValid(useBase64); 200 BinaryAttributeEditorPanel.this.attrName.setText(attrName); 201 setEnabledOK(true); 202 boolean isImage = hasImageSyntax(attrName); 203 setImageVisible(isImage); 204 if (value == null) 205 { 206 if (isImage) 207 { 208 useFile.setSelected(true); 209 } 210 else 211 { 212 useBase64.setSelected(true); 213 } 214 } 215 } 216 } 217 218 /** {@inheritDoc} */ 219 @Override 220 public Component getPreferredFocusComponent() 221 { 222 return file; 223 } 224 225 /** {@inheritDoc} */ 226 @Override 227 public void cancelClicked() 228 { 229 valueChanged = false; 230 super.cancelClicked(); 231 } 232 233 /** 234 * Returns the binary value displayed in the panel. 235 * @return the binary value displayed in the panel. 236 */ 237 public BinaryValue getBinaryValue() 238 { 239 return value; 240 } 241 242 /** {@inheritDoc} */ 243 @Override 244 public void okClicked() 245 { 246 refresh(true, false); 247 } 248 249 /** 250 * Refresh the contents in the panel. 251 * @param closeAndUpdateValue whether the dialog must be closed and the value 252 * updated at the end of the method or not. 253 * @param updateImage whether the displayed image must be updated or not. 254 */ 255 private void refresh(final boolean closeAndUpdateValue, 256 final boolean updateImage) 257 { 258 final ArrayList<LocalizableMessage> errors = new ArrayList<>(); 259 260 setPrimaryValid(useFile); 261 setPrimaryValid(useBase64); 262 263 final BinaryValue oldValue = value; 264 265 if (closeAndUpdateValue) 266 { 267 value = null; 268 } 269 270 if (useFile.isSelected()) 271 { 272 String f = file.getText(); 273 if (f.trim().length() == 0) 274 { 275 if (hasImageSyntax(attrName.getText()) && oldValue != null && !updateImage) 276 { 277 // Do nothing. We do not want to regenerate the image and we 278 // are on the case where the user simply did not change the image. 279 } 280 else 281 { 282 errors.add(ERR_CTRL_PANEL_FILE_NOT_PROVIDED.get()); 283 setPrimaryInvalid(useFile); 284 setPrimaryInvalid(lFile); 285 } 286 } 287 else 288 { 289 File theFile = new File(f); 290 if (!theFile.exists()) 291 { 292 errors.add(ERR_CTRL_PANEL_FILE_DOES_NOT_EXIST.get(f)); 293 setPrimaryInvalid(useFile); 294 setPrimaryInvalid(lFile); 295 } 296 else if (theFile.isDirectory()) 297 { 298 errors.add(ERR_CTRL_PANEL_PATH_IS_A_DIRECTORY.get(f)); 299 setPrimaryInvalid(useFile); 300 setPrimaryInvalid(lFile); 301 } 302 else if (!theFile.canRead()) 303 { 304 errors.add(ERR_CTRL_PANEL_CANNOT_READ_FILE.get(f)); 305 setPrimaryInvalid(useFile); 306 setPrimaryInvalid(lFile); 307 } 308 } 309 } 310 else 311 { 312 String b = base64.getText(); 313 if (b.length() == 0) 314 { 315 errors.add(ERR_CTRL_PANEL_VALUE_IN_BASE_64_REQUIRED.get()); 316 setPrimaryInvalid(useBase64); 317 } 318 } 319 if (errors.isEmpty()) 320 { 321 // Read the file or encode the base 64 content. 322 BackgroundTask<BinaryValue> worker = new BackgroundTask<BinaryValue>() 323 { 324 /** {@inheritDoc} */ 325 @Override 326 public BinaryValue processBackgroundTask() throws Throwable 327 { 328 try 329 { 330 Thread.sleep(1000); 331 } 332 catch (Throwable t) 333 { 334 } 335 BinaryValue returnValue; 336 if (useBase64.isSelected()) 337 { 338 returnValue = BinaryValue.createBase64(base64.getText()); 339 } 340 else if (file.getText().trim().length() > 0) 341 { 342 File f = new File(file.getText()); 343 FileInputStream in = null; 344 ByteArrayOutputStream out = new ByteArrayOutputStream(); 345 byte[] bytes = new byte[2 * 1024]; 346 try 347 { 348 in = new FileInputStream(f); 349 boolean done = false; 350 while (!done) 351 { 352 int len = in.read(bytes); 353 if (len == -1) 354 { 355 done = true; 356 } 357 else 358 { 359 out.write(bytes, 0, len); 360 } 361 } 362 returnValue = BinaryValue.createFromFile(out.toByteArray(), f); 363 } 364 finally 365 { 366 if (in != null) 367 { 368 in.close(); 369 } 370 out.close(); 371 } 372 } 373 else 374 { 375 // We do not want to regenerate the image and we 376 // are on the case where the user simply did not change the image. 377 returnValue = oldValue; 378 } 379 if (closeAndUpdateValue) 380 { 381 valueChanged = !returnValue.equals(oldValue); 382 } 383 if (updateImage) 384 { 385 updateImage(lImage, returnValue.getBytes()); 386 } 387 return returnValue; 388 } 389 390 /** {@inheritDoc} */ 391 @Override 392 public void backgroundTaskCompleted(BinaryValue returnValue, Throwable t) 393 { 394 setEnabledOK(true); 395 displayMainPanel(); 396 if (closeAndUpdateValue) 397 { 398 value = returnValue; 399 } 400 else 401 { 402 packParentDialog(); 403 } 404 if (t != null) 405 { 406 if (useFile.isSelected()) 407 { 408 errors.add(ERR_CTRL_PANEL_ERROR_READING_FILE.get(t)); 409 } 410 else 411 { 412 errors.add(ERR_CTRL_PANEL_ERROR_DECODING_BASE64.get(t)); 413 } 414 displayErrorDialog(errors); 415 } 416 else 417 { 418 if (closeAndUpdateValue) 419 { 420 Utilities.getParentDialog(BinaryAttributeEditorPanel.this). 421 setVisible(false); 422 } 423 } 424 } 425 }; 426 setEnabledOK(false); 427 displayMessage(INFO_CTRL_PANEL_READING_SUMMARY.get()); 428 worker.startBackgroundTask(); 429 } 430 else 431 { 432 displayErrorDialog(errors); 433 } 434 } 435 436 /** {@inheritDoc} */ 437 @Override 438 public LocalizableMessage getTitle() 439 { 440 return INFO_CTRL_PANEL_EDIT_BINARY_ATTRIBUTE_TITLE.get(); 441 } 442 443 /** {@inheritDoc} */ 444 @Override 445 public void configurationChanged(ConfigurationChangeEvent ev) 446 { 447 } 448 449 /** 450 * Returns whether the value has changed. 451 * 452 * @return {@code true} if the value has changed, {@code false} otherwise 453 */ 454 public boolean valueChanged() 455 { 456 return valueChanged; 457 } 458 459 /** {@inheritDoc} */ 460 @Override 461 public boolean requiresScroll() 462 { 463 return true; 464 } 465 466 /** 467 * Creates the layout of the panel (but the contents are not populated here). 468 */ 469 private void createLayout() 470 { 471 GridBagConstraints gbc = new GridBagConstraints(); 472 gbc.gridx = 0; 473 gbc.gridy = 0; 474 gbc.fill = GridBagConstraints.BOTH; 475 gbc.weightx = 0.0; 476 gbc.weighty = 0.0; 477 478 gbc.gridwidth = 1; 479 JLabel l = Utilities.createPrimaryLabel( 480 INFO_CTRL_PANEL_ATTRIBUTE_NAME_LABEL.get()); 481 add(l, gbc); 482 gbc.gridx ++; 483 gbc.insets.left = 10; 484 gbc.fill = GridBagConstraints.NONE; 485 gbc.anchor = GridBagConstraints.WEST; 486 attrName = Utilities.createDefaultLabel(); 487 gbc.gridwidth = 2; 488 add(attrName, gbc); 489 490 gbc.insets.top = 10; 491 gbc.insets.left = 0; 492 gbc.fill = GridBagConstraints.HORIZONTAL; 493 useFile = Utilities.createRadioButton( 494 INFO_CTRL_PANEL_USE_CONTENTS_OF_FILE.get()); 495 lFile = Utilities.createPrimaryLabel( 496 INFO_CTRL_PANEL_USE_CONTENTS_OF_FILE.get()); 497 useFile.setFont(ColorAndFontConstants.primaryFont); 498 gbc.gridx = 0; 499 gbc.gridy ++; 500 gbc.gridwidth = 1; 501 add(useFile, gbc); 502 add(lFile, gbc); 503 gbc.gridx ++; 504 file = Utilities.createLongTextField(); 505 gbc.weightx = 1.0; 506 gbc.insets.left = 10; 507 add(file, gbc); 508 gbc.gridx ++; 509 gbc.weightx = 0.0; 510 browse = Utilities.createButton(INFO_CTRL_PANEL_BROWSE_BUTTON_LABEL.get()); 511 browse.addActionListener( 512 new CustomBrowseActionListener(file, 513 BrowseActionListener.BrowseType.OPEN_GENERIC_FILE, this)); 514 browse.setOpaque(false); 515 add(browse, gbc); 516 gbc.gridy ++; 517 gbc.gridx = 0; 518 gbc.insets.left = 0; 519 gbc.gridwidth = 3; 520 useBase64 = Utilities.createRadioButton( 521 INFO_CTRL_PANEL_USE_CONTENTS_IN_BASE64.get()); 522 useBase64.setFont(ColorAndFontConstants.primaryFont); 523 add(useBase64, gbc); 524 525 gbc.gridy ++; 526 gbc.insets.left = 30; 527 gbc.fill = GridBagConstraints.BOTH; 528 gbc.weightx = 1.0; 529 base64 = Utilities.createLongTextField(); 530 add(base64, gbc); 531 532 imagePreview = 533 Utilities.createPrimaryLabel(INFO_CTRL_PANEL_IMAGE_PREVIEW_LABEL.get()); 534 gbc.gridy ++; 535 gbc.gridwidth = 1; 536 gbc.weightx = 0.0; 537 gbc.weighty = 0.0; 538 add(imagePreview, gbc); 539 540 refreshButton = Utilities.createButton( 541 INFO_CTRL_PANEL_REFRESH_BUTTON_LABEL.get()); 542 gbc.gridx ++; 543 gbc.insets.left = 5; 544 gbc.fill = GridBagConstraints.NONE; 545 add(refreshButton, gbc); 546 gbc.insets.left = 0; 547 gbc.weightx = 1.0; 548 add(Box.createHorizontalGlue(), gbc); 549 refreshButton.addActionListener(new ActionListener() 550 { 551 /** {@inheritDoc} */ 552 @Override 553 public void actionPerformed(ActionEvent ev) 554 { 555 refreshButtonClicked(); 556 } 557 }); 558 559 gbc.gridy ++; 560 gbc.gridwidth = 3; 561 gbc.insets.top = 5; 562 gbc.weightx = 0.0; 563 gbc.weighty = 0.0; 564 add(lImage, gbc); 565 566 addBottomGlue(gbc); 567 ButtonGroup group = new ButtonGroup(); 568 group.add(useFile); 569 group.add(useBase64); 570 571 ActionListener listener = new ActionListener() 572 { 573 @Override 574 public void actionPerformed(ActionEvent ev) 575 { 576 updateEnabling(); 577 } 578 }; 579 useFile.addActionListener(listener); 580 useBase64.addActionListener(listener); 581 } 582 583 /** 584 * Updates the enabling state of all the components in the panel. 585 * 586 */ 587 private void updateEnabling() 588 { 589 base64.setEnabled(useBase64.isSelected()); 590 file.setEnabled(useFile.isSelected()); 591 browse.setEnabled(useFile.isSelected()); 592 refreshButton.setEnabled(useFile.isSelected()); 593 } 594 595 /** 596 * Updates the provided component with the base 64 representation of the 597 * provided binary array. 598 * @param base64 the text component to be updated. 599 * @param bytes the byte array. 600 */ 601 static void updateBase64(JTextComponent base64, byte[] bytes) 602 { 603 if (bytes.length < MAX_BASE64_TO_DISPLAY) 604 { 605 BinaryValue value = BinaryValue.createBase64(bytes); 606 base64.setText(value.getBase64()); 607 } 608 else 609 { 610 base64.setText( 611 INFO_CTRL_PANEL_SPECIFY_CONTENTS_IN_BASE64.get().toString()); 612 } 613 } 614 615 /** 616 * Updates a label, by displaying the image in the provided byte array. 617 * @param lImage the label to be updated. 618 * @param bytes the array of bytes containing the image. 619 */ 620 static void updateImage(JLabel lImage, byte[] bytes) 621 { 622 Icon icon = Utilities.createImageIcon(bytes, 623 BinaryAttributeEditorPanel.MAX_IMAGE_HEIGHT, 624 INFO_CTRL_PANEL_IMAGE_OF_ATTRIBUTE_LABEL.get(), false); 625 if (icon.getIconHeight() > 0) 626 { 627 lImage.setIcon(icon); 628 lImage.setText(""); 629 } 630 else 631 { 632 Utilities.setWarningLabel(lImage, 633 INFO_CTRL_PANEL_PREVIEW_NOT_AVAILABLE_LABEL.get()); 634 } 635 } 636 637 /** 638 * Updates the visibility of the components depending on whether the image 639 * must be made visible or not. 640 * @param visible whether the image must be visible or not. 641 */ 642 private void setImageVisible(boolean visible) 643 { 644 imagePreview.setVisible(visible); 645 refreshButton.setVisible(visible); 646 lFile.setVisible(visible); 647 useFile.setVisible(!visible); 648 useBase64.setVisible(!visible); 649 base64.setVisible(!visible); 650 lImage.setVisible(visible); 651 } 652 653 /** 654 * Class used to refresh automatically the contents in the panel after the 655 * user provides a path value through the JFileChooser associated with the 656 * browse button. 657 * 658 */ 659 class CustomBrowseActionListener extends BrowseActionListener 660 { 661 /** 662 * Constructor of this listener. 663 * @param field the text field. 664 * @param type the type of browsing (file, directory, etc.) 665 * @param parent the parent component to be used as reference to display 666 * the file chooser dialog. 667 */ 668 public CustomBrowseActionListener(JTextComponent field, BrowseType type, 669 Component parent) 670 { 671 super(field, type, parent); 672 } 673 674 /** {@inheritDoc} */ 675 @Override 676 protected void fieldUpdated() 677 { 678 super.fieldUpdated(); 679 if (refreshButton.isVisible()) 680 { 681 // The file field is updated, if refreshButton is visible it means 682 // that we can have a preview. 683 refreshButtonClicked(); 684 } 685 } 686 } 687 688 /** 689 * Called when the refresh button is clicked by the user. 690 * 691 */ 692 private void refreshButtonClicked() 693 { 694 refresh(false, true); 695 } 696 697 /** 698 * Returns <CODE>true</CODE> if the attribute has an image syntax and 699 * <CODE>false</CODE> otherwise. 700 * @param attrName the attribute name. 701 * @return <CODE>true</CODE> if the attribute has an image syntax and 702 * <CODE>false</CODE> otherwise. 703 */ 704 private boolean hasImageSyntax(String attrName) 705 { 706 Schema schema = getInfo().getServerDescriptor().getSchema(); 707 return Utilities.hasImageSyntax(attrName, schema); 708 } 709}