< Previous | Next >

Add or remove attachments

In this lesson, you learn how to modify field values of a record and add or remove attachments.

About this task

The code example in this lesson allows a user to update the record she is viewing. This example extends the ViewRecord example in the previous module by extending the ViewRecord.Viewer class to support a combo box for selecting an action and an additional Edit button for initiating the selected action on the record. The editRecord method of this Viewer orchestrates the viewing and updating of a record opened for update. These new GUI components are introduced into the ViewRecord.Viewer display using the future argument to showRecord.

The view(CqRecord) method now reads the LEGAL_ACTIONS property of the record in addition to the ALL_FIELD_VALUES property. The value of the LEGAL_ACTIONS property is a list of Action proxies for the actions that may legally be applied to the record in its current state. This list is used to populate a combo box control, from which the user may select an action prior to clicking the edit button. Some types of actions, such as SUBMIT and RECORD_SCRIPT_ALIAS are not supported by this example and are, therefore, not added to the combo box control even if present in the set of legal actions.

public JFrame view(CqRecord selected)
{
    try {
        final CqRecord record =
                (CqRecord)selected.doReadProperties(RECORD_PROPERTIES);
        final JButton start = new JButton("Edit");
        final JComboBox choices = new JComboBox();

        for (CqAction a: record.getLegalActions()) {
            if (a.getType() != CqAction.Type.IMPORT
                && a.getType() != CqAction.Type.SUBMIT
                && a.getType() != CqAction.Type.BASE
                && a.getType() != CqAction.Type.RECORD_SCRIPT_ALIAS)
                choices.addItem(a);
        }
        
        final JButton add = new JButton("Attach");
        final JButton remove = new JButton("Detach");
        final ViewRecord.RecordFrame frame = 
            showRecord("View: ", record, choices.getItemCount()==0? null: 
                         new JComponent[]{choices, start, add, remove});

The EditView.Viewer also supports an Attach and a Detach button for adding and removing the attached files of an attachment field.

The Attach button presents a standard Swing file-chooser dialog to the user and allows him to select the file to be attached. To attach the file to the record, CqAttachment.doCreateAttachment() is used, passing it the name of the file selected by the user. Before this method can be invoked a CqAttachment proxy addressing the proper location must be constructed. The folder in which the attachment resource will reside is the value of the field to which it is attached - this is the field currently selected in the record view and so it is obtained from the frame object returned by ViewRecord.showRecord. A unique name for the attachment resource is generated from the current time of day. This name is merely a placeholder since doCreateAttachment() is free to change the name of the resource to suit its needs.

add.addActionListener(new ActionListener(){
            /**
             * Prompts the user for the name of a file to be attached to
             * the selected field. If so provided, the content of the 
             * file is read into the Rational
ClearQuest database as an attachment.
             */
            public void actionPerformed(ActionEvent arg0)
            {
                if (!ViewRecord.isAttachmentList(frame.m_fields, 
                                                 frame.m_table.getSelectedRow()))
                    return;

                JFileChooser chooser = new JFileChooser();

                if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
                 try {
                   String filename =  chooser.getSelectedFile().getAbsolutePath();
                   CqFieldValue field = frame.m_fields.get(frame.m_table.getSelectedRow());
                   StpLocation aLoc = (StpLocation) ((StpFolder)field.getValue()).location()
                                     .child("new" + System.currentTimeMillis());
                   CqAttachment attachment = record.cqProvider().cqAttachment(aLoc);
                       
                       attachment = attachment.doCreateAttachment(filename, 
                                                                  null, 
                                                                  CqProvider.DELIVER);
                       
                       JOptionPane.showMessageDialog(frame, 
                                                  "Added '" + filename + "' as " + attachment);
                       frame.dispose();
                       view(record);
                    } catch(Throwable t) 
                        { Utilities.exception(frame, "Add Attachment", t);}
                }}});

The Detach button uses ViewRecord.selectAttachment to get from the user the identity of the attachment to be removed in the form of an CqAttachment proxy. The doUnbindAll method of this proxy is then invoked to remove the attachment from the database.

        remove.addActionListener(new ActionListener(){
            /**
             * Allows the user to select an attachment associated with
             * the selected field. If the user selects such an attachment
             * it is deleted.
             */
            public void actionPerformed(ActionEvent arg0)
            {
             if (!ViewRecord.isAttachmentList(frame.m_fields, frame.m_table.getSelectedRow()))
                 return;
                
                try {
                   CqAttachment attachment = ViewRecord.selectAttachment
                       (frame, frame.m_fields, frame.m_table.getSelectedRow(), "Remove");
                   
                   if (attachment != null) {
                       attachment.doUnbindAll(null);
                       frame.dispose();
                       view(record);
                   }
                } catch(Throwable t) 
                    { Utilities.exception(frame, "Remove Attachment", t);}
                }
            }
        );

The Edit button fetches the selected CqAction proxy from the combo box and passes it and the record proxy for the current viewer to the edit method, which will actually initiate the editing of the record. If the CqAction.Type of the selected action is DUPLICATE, then the id of the duplicated record must be supplied with the action. This value is requested from the user and is placed into the Action's argument map as the value of the argument named original.

        start.addActionListener(new ActionListener(){
            /**
             * Starts editing of the record using the action currently
             * selected by the combo box on the view dialog.
             */
            public void actionPerformed(ActionEvent arg0)
            {
                CqAction action = (CqAction)choices.getSelectedItem();
                
                try {
                    if (action.getType() == CqAction.Type.DUPLICATE) {
                        String id = JOptionPane.showInputDialog (frame, "Enter ID of duplicated record");
                        
                        if (id == null) return;
                        
                        action.argumentMap(new Hashtable<String>());
                        action.argumentMap().put("original", 
                                 			  record.cqProvider().cqRecord((StpLocation)record
                                                           .location().parent().child(id)));
                    }

                    edit(record, action);
                    frame.dispose();
                } catch (Exception ex) {
                    Utilities.exception(frame, "Duplicate Action", ex);
                }
            }
        });
        
        return frame;
    } catch (WvcmException ex){
        ex.printStackTrace();
    }
    
    return null;
}

The edit method is invoked when the user clicks the Edit button in the record viewer. It is passed a proxy for the record to be edited and a proxy for the action under which the edit is to be performed.

Note that no properties need to be defined by the Action proxy used to start a Rational® ClearQuest® action - only its location and its argument map (if needed) must be defined. To form the location for an action, you need to know its <name>, the <record-type> of the record it's going to be used with, and the <database> and <db-set> where the record is located.

The location is then cq.action:<record-type>/<name>@<db-set>/<database>; an example is, cq.action:Defect/Assign@7.0.0/SAMPL

Using a new proxy, CqRecord.doWriteProperties is invoked to begin the edit operation. The only property that is to be written is the action under which the record is to be edited. The doWriteProperties method returns a proxy for the record that was opened for edit and that proxy will contain the properties requested in the RECORD_PROPERTIES PropertyRequest. The properties could also be obtained using record.doReadProperties (with the same PropertyRequest) after doWriteProperties returns, but that would be less efficient as it would require another roundtrip to the repository for the same data.

        public void edit(CqRecord selected, CqAction action)
        {
            try {
                CqRecord record = m_provider.cqRecord(selected.stpLocation());
        
                record =
                    (CqRecord) record.setAction(action)
                        .doWriteProperties(RECORD_PROPERTIES,
                                           CqProvider.HOLD);
                editRecord("Edit: ", record, selected);
            } catch (WvcmException ex){
                Utilities.exception(null, "Start Action", ex);
            }
        }

Viewer.editRecord is similar to showRecord. The primary difference is that it defines TableModel.isCellEditable and TableModel.setValue.

JFrame editRecord(String title, 
                                final CqRecord record, 
                                final CqRecord selected) throws WvcmException 
             {
        final JFrame frame = new JFrame(title + record.getUserFriendlyLocation());
        JPanel panel = new JPanel(new BorderLayout());
        JPanel buttons = new JPanel(new FlowLayout());
        final JButton show = new JButton("View");
        final JButton add = new JButton("Add");
        final JButton remove = new JButton("Remove");
        final JButton cancel = new JButton("Cancel");
        final StpProperty.List<CqFieldValue>> fields = record.getAllFieldValues();
        
        TableModel dataModel = new AbstractTableModel() {
        public int getColumnCount() { return fieldMetaProperties.length; }
        public int getRowCount() { return fields.size();}
        public String getColumnName(int col) 
             { return fieldMetaProperties[col].getRoot().getName(); }
        public Object getValueAt(int row, int col) 
            { 
                try {
                    return fields.get(row).getMetaProperty((MetaPropertyName<?>)
                                                            fieldMetaProperties[col].getRoot());
                } catch(Throwable ex) {
                    if (ex instanceof StpException) {
                        return ((StpException)ex).getStpReasonCode();  
                      } else {
                          String name = ex.getClass().getName();
                          return name.substring(name.lastIndexOf(".")+1);
                      }
                }
            }
        

TableModel.isCellEditable returns true only for the VALUE column and only if the row belongs to a field whose REQUIREDNESS is not READ_ONLY.

        public boolean isCellEditable(int row, int col) {
            if (fieldMetaProperties[col].getRoot().equals(StpProperty.VALUE)) {
                CqFieldValue field = fields.get(row);
                
                try {
                    return field.getRequiredness() != Requiredness.READ_ONLY;
                } catch (WvcmException ex) {
                    Utilities.exception(frame, "Field Requiredness", ex);
                }
            }
            
            return false;
        }

TableModel.setValueAt sets the new field value into the CqRecord proxy associated with the display. First, the CqFieldValue structure is updated using its initialize() method. This method accepts a string representation for most types of fields and will convert the string into the appropriate data type for the field.

Once the CqFieldValue has been updated, it is set into the CqRecord proxy associated with the PropertyName for the field. This step is necessary so that the new field value gets written to the database when the record is committed. Without this step, the new field value would remain only in the CqFieldValue object that is part of the ALL_FIELD_VALUES property. The ALL_FIELD_VALUES property is not writeable, so the change would never get written to the database. By copying the modified CqFieldValue directly to the proxy entry for the field, that field becomes an updated property and the updated value will be written to the database by the next do method that is executed on the proxy.

        public void setValueAt(Object aValue, int row, int col)
        {
            if (fieldMetaProperties[col].getRoot().equals(StpProperty.VALUE)) {
                CqFieldValue<Object> field = fields.get(row);
                
                field.initialize(aValue);
                record.setFieldInfo(field.getFieldName(), field);
            }
        }

        private static final long serialVersionUID = 1L;
    };

    final JTable table = new JTable(dataModel);

    table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    show.setEnabled(false);
    add.setEnabled(false);
    remove.setEnabled(false);
    
    // Ask to be notified of selection changes.
    ListSelectionModel rowSM = table.getSelectionModel();
    rowSM.addListSelectionListener(new ListSelectionListener() {
        /**
         * Enables the buttons in the dialog based on the type of
         * field selected in the table.
         */
        public void valueChanged(ListSelectionEvent e) {
            if (!e.getValueIsAdjusting()){
                int[] selected = table.getSelectedRows();
                show.setEnabled(false);
                add.setEnabled(false);
                remove.setEnabled(false);
                for (int i=0; i <selected.length; ++i)
                    if (ViewRecord.getRecordReferencedAt(fields, selected[i]) != null) {
                        show.setEnabled(true);
                    } else if (ViewRecord.isAttachmentList(fields, selected[i])) {
                        show.setEnabled(true);
                        add.setEnabled(true);
                        remove.setEnabled(true);
                    }
            }
        }
    });

The View button is the same as the View button in a ViewRecord.Viewer.

    buttons.add(show);
    show.addActionListener(new ActionListener(){
            /**
             * Invokes a view method on the value of the field selected
             * in the table.
             */
            public void actionPerformed(ActionEvent arg0)
            {
                int[] selected = table.getSelectedRows();
                
                for (int i =0; i < selected.length; ++i) {
                    int row = selected[i];
                    CqRecord record = ViewRecord.getRecordReferencedAt(fields, row);
                    
                    if (record != null) {
                        view(record);
                    } else if (ViewRecord.isAttachmentList(fields, row)) {
                        view(ViewRecord.selectAttachment(frame, fields, row, "View"));
                    }
                }
            }
        });

The Deliver button commits the modified record to the database by calling doWriteProperties() with the CqProvider.DELIVER option. After the delivery succeeds, the Viewer is taken down and then a new Viewer is brought up on the original proxy. Since the view() method rereads properties from the database, this new view will display the updated values.

    JButton deliver = new JButton("Deliver");

    buttons.add(deliver);
    deliver.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent arg0)
            {
                try {
                    int mode = frame.getDefaultCloseOperation();
                    
                    record.doWriteProperties(null, CqProvider.DELIVER);
                    frame.dispose();
                    view(selected).setDefaultCloseOperation(mode);
                } catch (WvcmException ex) {
                    Utilities.exception(frame, "Deliver failed", ex);
                }
            }
        });

The Cancel button abandons the edit operation using the doRevert method of the record proxy. As with the Deliver button, the Viewer is closed and a new Viewer is instantiated for the original proxy.

    buttons.add(cancel);
    cancel.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent arg0)
            {
                try {
                    int mode = frame.getDefaultCloseOperation();
                    
                    record.doRevert(null);
                    frame.dispose();
                    
                    if (mode == JFrame.EXIT_ON_CLOSE)
                        System.exit(0);
                    
                    view(selected).setDefaultCloseOperation(mode);
                } catch (WvcmException ex) {
                    Utilities.exception(frame, "Cancel failed", ex);
                }
            }
        });

The Attach and Detach buttons are the same as those for the ViewRecord.Viewer, except that the delivery order argument is HOLD instead of DELIVER. This will defer committing the addition or deletion of the attachment until the entire record is committed by the Deliver button.

    buttons.add(add);
    add.addActionListener(new ActionListener(){
        public void actionPerformed(ActionEvent arg0)
        {
            JFileChooser chooser = new JFileChooser();
            int returnVal = chooser.showOpenDialog(frame);

            if (returnVal == JFileChooser.APPROVE_OPTION) {
                try {
                   String filename = chooser.getSelectedFile().getAbsolutePath();
                   CqFieldValue field = fields.get(table.getSelectedRow());
                   StpLocation aLoc = (StpLocation) ((StpFolder)field.getValue()).location()
                                                                      .child("new" + System.currentTimeMillis());
                   CqAttachment attachment = record.cqProvider().cqAttachment(aLoc);
                   
                   attachment = attachment.doCreateAttachment(filename, 
                                                                       null, 
                                                                       CqProvider.HOLD);
                   
                   JOptionPane.showMessageDialog(frame, 
                                                      "Added '" + filename + "' as " + attachment 
                                                    + " (pending delivery)");
                } catch(Throwable t) 
                    { Utilities.exception(frame, "Add Attachment", t);}
            }}});
    
    buttons.add(remove);
    remove.addActionListener(new ActionListener(){
        public void actionPerformed(ActionEvent arg0)
        {
            try {
               CqAttachment attachment = ViewRecord.selectAttachment
                                                   (frame, fields, table.getSelectedRow(), "Remove");
               if (attachment != null)
                   attachment.doUnbindAll(null);
            } catch(Throwable t) 
                { Utilities.exception(frame, "Remove Attachment", t);}
            }
        }
    );
    
    panel.add(new JScrollPane(table), BorderLayout.CENTER);
    panel.add(buttons, BorderLayout.SOUTH);
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    frame.setContentPane(panel);
    frame.setBounds(300, 300, 600, 300);
    frame.setVisible(true);
    
    return frame;
}

Results

In this example, the edited field values are kept on the client until the user selects the Deliver button. Each field modified by the user marks the corresponding property in the record proxy as having been updated. These updated properties are written to the server as the first step of the deliver operation. A problem with this approach is that the new field values are not checked until the final delivery, which could fail if the values are not appropriate for the schema being used.

The GUI could perform a call to record.doWriteProperties each time a field is modified, which would give the user immediate feedback, but might also be terribly inefficient depending on the communication protocol between the client and the server. The GUI could also provide an Update button, which would cause all accumulated updates to be written to the server, without actually delivering them. This would give the schema an opportunity to examine the values and report back errors. These modifications are left to the reader and which approach is taken would depend on the intended use of this application.

Lesson checkpoint

You have now learned how to use the Rational ClearQuest CM API for developing client application actions that perform operations on records in a user database. The final lesson of this tutorial demonstrates how to create a new record.
In this lesson, you learned the following:
  • About using the Rational ClearQuest CM API to perform operations on resources in a user database from a client application.
  • How to create product-specific operations from a client application by using the Rational ClearQuest CM API.
< Previous | Next >