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}