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}