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