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