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}