001    /*
002     * file EditRecord.java
003     *
004     * Licensed Materials - Property of IBM
005     * Restricted Materials of IBM - you are allowed to copy, modify and 
006     * redistribute this file as part of any program that interfaces with 
007     * IBM Rational CM API.
008     *
009     * com.ibm.rational.stp.client.samples.EditRecord
010     *
011     * © Copyright IBM Corporation 2005, 2008.  All Rights Reserved.
012     * Note to U.S. Government Users Restricted Rights:  Use, duplication or 
013     * disclosure restricted by GSA ADP  Schedule Contract with IBM Corp.
014     */
015    package com.ibm.rational.stp.client.samples;
016    
017    import java.awt.BorderLayout;
018    import java.awt.FlowLayout;
019    import java.awt.event.ActionEvent;
020    import java.awt.event.ActionListener;
021    import java.util.Hashtable;
022    
023    import javax.swing.JButton;
024    import javax.swing.JComboBox;
025    import javax.swing.JComponent;
026    import javax.swing.JFileChooser;
027    import javax.swing.JFrame;
028    import javax.swing.JOptionPane;
029    import javax.swing.JPanel;
030    import javax.swing.JScrollPane;
031    import javax.swing.JTable;
032    import javax.swing.ListSelectionModel;
033    import javax.swing.event.ListSelectionEvent;
034    import javax.swing.event.ListSelectionListener;
035    import javax.swing.table.AbstractTableModel;
036    import javax.swing.table.TableModel;
037    import javax.wvcm.Folder;
038    import javax.wvcm.PropertyRequestItem;
039    import javax.wvcm.WvcmException;
040    import javax.wvcm.PropertyNameList.PropertyName;
041    import javax.wvcm.PropertyRequestItem.PropertyRequest;
042    
043    import com.ibm.rational.wvcm.stp.StpException;
044    import com.ibm.rational.wvcm.stp.StpLocation;
045    import com.ibm.rational.wvcm.stp.StpProperty;
046    import com.ibm.rational.wvcm.stp.StpProperty.MetaPropertyName;
047    import com.ibm.rational.wvcm.stp.cq.CqAction;
048    import com.ibm.rational.wvcm.stp.cq.CqAttachment;
049    import com.ibm.rational.wvcm.stp.cq.CqFieldDefinition;
050    import com.ibm.rational.wvcm.stp.cq.CqFieldValue;
051    import com.ibm.rational.wvcm.stp.cq.CqProvider;
052    import com.ibm.rational.wvcm.stp.cq.CqRecord;
053    
054    /**
055     * A sample CM API application that allows the user to browse to a record by
056     * executing a query and selecting a record from the result set. Within the
057     * record display the fields of the record can be modified under an applicable
058     * action.
059     */
060    public class EditRecord {
061        /**
062         * An extension of the ViewRecord.Viewer that adds the ability to start an
063         * action under which fields of the record can be modified.
064         */
065        static class Viewer extends ViewRecord.Viewer {
066            Viewer(CqProvider provider) { super(provider); }
067            
068            /**
069             * Reads the legal actions for the current state of a given record
070             * then displays the fields of the record 
071             * (using {@link ViewRecord.Viewer#showRecord}) with a drop-down 
072             * containing the actions and buttons the user can use to initiate an
073             * edit or add or remove an attachment.
074             * @see com.ibm.rational.stp.client.samples.ExecuteQuery.Viewer#view(com.ibm.rational.wvcm.stp.cq.CqRecord)
075             */
076            public JFrame view(CqRecord selected)
077            {
078                try {
079                    final CqRecord record =
080                            (CqRecord)selected.doReadProperties(RECORD_PROPERTIES);
081                    final JButton start = new JButton("Edit");
082                    final JComboBox choices = new JComboBox();
083            
084                    for (CqAction a: record.getLegalActions()) {
085                        if (a.getType() != CqAction.Type.IMPORT
086                            && a.getType() != CqAction.Type.SUBMIT
087                            && a.getType() != CqAction.Type.BASE
088                            && a.getType() != CqAction.Type.RECORD_SCRIPT_ALIAS)
089                            choices.addItem(a);
090                    }
091                    
092                    final JButton add = new JButton("Attach");
093                    final JButton remove = new JButton("Detach");
094                    final ViewRecord.RecordFrame frame = 
095                        showRecord("View: ", record, choices.getItemCount()==0? null: 
096                                     new JComponent[]{choices, start, add, remove});
097                    
098                    add.addActionListener(new ActionListener(){
099                        /**
100                         * Prompts the user for the name of a file to be attached to
101                         * the selected field. If so provided, the content of the 
102                         * file is read into the ClearQuest database as an attachment.
103                         */
104                        public void actionPerformed(ActionEvent arg0)
105                        {
106                            if (!ViewRecord.isAttachmentList(frame.m_fields, 
107                                                             frame.m_table.getSelectedRow()))
108                                return;
109            
110                            JFileChooser chooser = new JFileChooser();
111            
112                            if (chooser.showOpenDialog(frame) == 
113                                    JFileChooser.APPROVE_OPTION) {
114                                try {
115                                   String filename = 
116                                       chooser.getSelectedFile().getAbsolutePath();
117                                   CqFieldValue field = 
118                                       frame.m_fields.get(frame.m_table.getSelectedRow());
119                                   StpLocation aLoc = (StpLocation) 
120                                       ((Folder)field.getValue()).location()
121                                           .child("new" + System.currentTimeMillis());
122                                   CqAttachment attachment = 
123                                       record.cqProvider().cqAttachment(aLoc);
124                                   
125                                   attachment = attachment
126                                       .doCreateAttachment(filename, 
127                                                           null, 
128                                                           CqProvider.DELIVER_ALL);
129                                   
130                                   JOptionPane.showMessageDialog(frame, 
131                                      "Added '" + filename + "' as " + attachment);
132                                   
133                                   frame.dispose();
134                                   view(record);
135                                } catch(Throwable t) 
136                                    { Utilities.exception(frame, "Add Attachment", t);}
137                            }}});
138                    
139                    remove.addActionListener(new ActionListener(){
140                        /**
141                         * Allows the user to select an attachment associated with
142                         * the selected field. If the user selects such an attachment
143                         * it is deleted.
144                         */
145                        public void actionPerformed(ActionEvent arg0)
146                        {
147                            if (!ViewRecord.isAttachmentList(frame.m_fields, frame.m_table.getSelectedRow()))
148                                return;
149                            
150                            try {
151                               CqAttachment attachment = ViewRecord.selectAttachment
152                                   (frame, frame.m_fields, frame.m_table.getSelectedRow(), "Remove");
153                               
154                               if (attachment != null) {
155                                   attachment.doUnbindAll(null);
156                                   frame.dispose();
157                                   view(record);
158                               }
159                            } catch(Throwable t) 
160                                { Utilities.exception(frame, "Remove Attachment", t);}
161                            }
162                        }
163                    );
164            
165                    start.addActionListener(new ActionListener(){
166                        /**
167                         * Starts editing of the record using the action currently
168                         * selected by the combo box on the view dialog.
169                         */
170                        public void actionPerformed(ActionEvent arg0)
171                        {
172                            CqAction action = (CqAction)choices.getSelectedItem();
173                            
174                            try {
175                                if (action.getType() == CqAction.Type.DUPLICATE) {
176                                    String id = JOptionPane.showInputDialog
177                                        (frame, "Enter ID of duplicated record");
178                                    
179                                    if (id == null) return;
180                                    
181                                    action.argumentMap(new Hashtable<String, Object>());
182                                    action.argumentMap()
183                                        .put("original", 
184                                             record.cqProvider()
185                                                 .cqRecord((StpLocation)record
186                                                 .location().parent().child(id)));
187                                }
188            
189                                edit(record, action);
190                                frame.dispose();
191                            } catch (Exception ex) {
192                                Utilities.exception(frame, "Duplicate Action", ex);
193                            }
194                        }
195                    });
196                    
197                    return frame;
198                } catch (WvcmException ex){
199                    ex.printStackTrace();
200                }
201                
202                return null;
203            }
204    
205            /**
206             * Constructs a new change context in which the given record can be
207             * edited, initiates the editing using the given action, and then
208             * displays the editable record in a new window for editing.
209             * @param selected A Record proxy for the record to be edited. Must not
210             * be null but needs no properties defined.
211             * @param action An Action proxy for the action to be used for editing.
212             * Must not be null. 
213             */
214            public void edit(CqRecord selected, CqAction action)
215            {
216                try {
217                    CqProvider context = m_provider;
218                    CqRecord record = context.cqRecord(selected.stpLocation());
219            
220                    record =
221                        (CqRecord) record.setAction(action)
222                            .doWriteProperties(RECORD_PROPERTIES,
223                                               CqProvider.HOLD);
224                    editRecord("Edit: ", record, selected);
225                } catch (WvcmException ex){
226                    Utilities.exception(null, "Start Action", ex);
227                }
228            }
229            
230            /**
231             * Displays the fields of a record and allows the user to modify those
232             * fields (as permitted by the schema) until the changes are either
233             * committed back to the database or abandoned altogether by the user.
234             * @param title The title string for the window in which the record is
235             * displayed.
236             * @param record The Record proxy for the editable version of the record.
237             * @param selected The Record proxy for the original version of the
238             * record. Used to redisplay the record (in its original change context)
239             * after the editable version has either been committed or abandoned.
240             * @throws WvcmException If the properties required for the editable
241             * record are not defined by the editable record proxy. 
242             */
243            JFrame editRecord(
244                final String title, 
245                final CqRecord record, 
246                final CqRecord selected
247                ) throws WvcmException 
248           {
249                final JFrame frame = 
250                    new JFrame(title + record.getUserFriendlyLocation().toString());
251                JPanel panel = new JPanel(new BorderLayout());
252                JPanel buttons = new JPanel(new FlowLayout());
253                final JButton show = new JButton("View");
254                final JButton add = new JButton("Add");
255                final JButton remove = new JButton("Remove");
256                final JButton cancel = new JButton("Cancel");
257                final StpProperty.List<CqFieldValue<?>> fields = 
258                    record.getAllFieldValues();
259                TableModel dataModel = new AbstractTableModel() {
260                    public int getColumnCount() { return fieldMetaProperties.length; }
261                    public int getRowCount() { return fields.size();}
262                    public String getColumnName(int col)
263                    { return fieldMetaProperties[col].getRoot().getName(); }
264                    public Object getValueAt(int row, int col) 
265                        { 
266                            try {
267                                return fields.get(row)
268                                    .getMetaProperty((MetaPropertyName<?>)
269                                                     fieldMetaProperties[col].getRoot());
270                            } catch(Throwable ex) {
271                                if (ex instanceof StpException) {
272                                    return ((StpException)ex).getStpReasonCode();  
273                                  } else {
274                                      String name = ex.getClass().getName();
275                                      return name.substring(name.lastIndexOf(".")+1);
276                                  }
277                            }
278                        }
279                    
280                    /**
281                     * Determines whether or not the specified cell should be 
282                     * editable. A cell is editable only if it corresponds to the
283                     * VALUE meta-property of a field whose REQUIREDNESS is not
284                     * READ_ONLY
285                     */ 
286                    public boolean isCellEditable(int row, int col) {
287                        if (fieldMetaProperties[col].getRoot()
288                                        .equals(StpProperty.VALUE)) {
289                            CqFieldValue field = (CqFieldValue)fields.get(row);
290                            
291                            try {
292                                return field.getRequiredness() 
293                                        != CqFieldDefinition.Requiredness.READ_ONLY;
294                            } catch (WvcmException ex) {
295                                Utilities.exception(frame, "Field Requiredness", ex);
296                            }
297                        }
298                        
299                        return false;
300                    }
301                    
302                    /**
303                     * Sets a new value into the record field that corresponds to
304                     * the specified row. Uses CqFieldValue.initialize() to interpret
305                     * the value with respect to the type of the field and then
306                     * sets the updated CqFieldValue into the proxy for writing to
307                     * the repository. 
308                     */
309                    public void setValueAt(Object aValue, int row, int col)
310                    {
311                        if (fieldMetaProperties[col].getRoot()
312                                        .equals(StpProperty.VALUE)) {
313                            CqFieldValue<Object> field = 
314                                (CqFieldValue<Object>)fields.get(row);
315                            
316                            try {
317                                field.initialize(aValue);
318                            } catch (WvcmException e) {
319                                Utilities.exception(frame, title, e);
320                            }
321                            record.setFieldInfo(field.getFieldName(), field);
322                        }
323                    }
324                    private static final long serialVersionUID = 1L;
325                };
326                final JTable table = new JTable(dataModel);
327    
328                table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
329                show.setEnabled(false);
330                add.setEnabled(false);
331                remove.setEnabled(false);
332                
333                // Ask to be notified of selection changes.
334                ListSelectionModel rowSM = table.getSelectionModel();
335                rowSM.addListSelectionListener(new ListSelectionListener() {
336                    /**
337                     * Enables the buttons in the dialog based on the type of
338                     * field selected in the table.
339                     */
340                    public void valueChanged(ListSelectionEvent e) {
341                        if (!e.getValueIsAdjusting()){
342                            int[] selected = table.getSelectedRows();
343                            show.setEnabled(false);
344                            add.setEnabled(false);
345                            remove.setEnabled(false);
346                            for (int i=0; i <selected.length; ++i)
347                                if (ViewRecord.getRecordReferencedAt(fields, selected[i]) != null) {
348                                    show.setEnabled(true);
349                                } else if (ViewRecord.isAttachmentList(fields, selected[i])) {
350                                    show.setEnabled(true);
351                                    add.setEnabled(true);
352                                    remove.setEnabled(true);
353                                }
354                        }
355                    }
356                });
357    
358                buttons.add(show);
359                show.addActionListener(new ActionListener(){
360                        /**
361                         * Invokes a view method on the value of the field selected
362                         * in the table.
363                         */
364                        public void actionPerformed(ActionEvent arg0)
365                        {
366                            int[] selected = table.getSelectedRows();
367                            
368                            for (int i =0; i < selected.length; ++i) {
369                                int row = selected[i];
370                                CqRecord record = 
371                                    ViewRecord.getRecordReferencedAt(fields, row);
372                                
373                                if (record != null) {
374                                    view(record);
375                                } else if (ViewRecord.isAttachmentList(fields, row)) {
376                                    view(ViewRecord.selectAttachment(frame, 
377                                                                  fields, row, 
378                                                                  "View"));
379                                }
380                            }
381                        }
382                    });
383        
384                JButton deliver = new JButton("Deliver");
385    
386                buttons.add(deliver);
387                deliver.addActionListener(new ActionListener(){
388                        /**
389                         * Delivers the modified record to the repository,
390                         * closes the (now empty) change context, deletes the
391                         * current viewer, and opens a new viewer on the updated
392                         * record (in its original change context).
393                         */
394                        public void actionPerformed(ActionEvent arg0)
395                        {
396                            try {
397                                int mode = frame.getDefaultCloseOperation();
398                                
399                                record.doWriteProperties(null, CqProvider.DELIVER);
400                                frame.dispose();
401                                view(selected).setDefaultCloseOperation(mode);
402                            } catch (WvcmException ex) {
403                                Utilities.exception(frame, "Deliver failed", ex);
404                            }
405                        }
406                    });
407        
408                buttons.add(cancel);
409                cancel.addActionListener(new ActionListener(){
410                        /**
411                         * Discards all modifications to the record,
412                         * closes the (now empty) change context, deletes the
413                         * current viewer, and opens a new viewer on the original
414                         * record (in its original change context).
415                         */
416                        public void actionPerformed(ActionEvent arg0)
417                        {
418                            try {
419                                int mode = frame.getDefaultCloseOperation();
420                                
421                                record.doRevert(null);
422                                frame.dispose();
423                                
424                                if (mode == JFrame.EXIT_ON_CLOSE)
425                                    System.exit(0);
426                                
427                                view(selected)
428                                    .setDefaultCloseOperation(mode);
429                            } catch (WvcmException ex) {
430                                Utilities.exception(frame, "Cancel failed", ex);
431                            }
432                        }
433                    });
434        
435                buttons.add(add);
436                add.addActionListener(new ActionListener(){
437                    /**
438                     * Adds an file specified by the user to the database as an
439                     * attachment to the selected record field.
440                     */
441                    public void actionPerformed(ActionEvent arg0)
442                    {
443                        JFileChooser chooser = new JFileChooser();
444                        int returnVal = chooser.showOpenDialog(frame);
445    
446                        if (returnVal == JFileChooser.APPROVE_OPTION) {
447                            try {
448                               String filename = 
449                                   chooser.getSelectedFile().getAbsolutePath();
450                               CqFieldValue field = 
451                                   (CqFieldValue)fields.get(table.getSelectedRow());
452                               StpLocation aLoc = (StpLocation) 
453                                   ((Folder)field.getValue()).location()
454                                       .child("new" + System.currentTimeMillis());
455                               CqAttachment attachment = 
456                                   ((CqProvider)record.provider()).cqAttachment(aLoc);
457                               
458                               attachment = attachment
459                                   .doCreateAttachment(filename, 
460                                                              null, CqProvider.HOLD);
461                               
462                               JOptionPane.showMessageDialog(frame, 
463                                  "Added '" + filename + "' as " + attachment 
464                                  + " (pending delivery)");
465                            } catch(Throwable t) 
466                                { Utilities.exception(frame, "Add Attachment", t);}
467                        }}});
468                
469                buttons.add(remove);
470                remove.addActionListener(new ActionListener(){
471                    /**
472                     * Removes a user-specified entry from the attachment folder 
473                     * of the selected attachment field
474                     */
475                    public void actionPerformed(ActionEvent arg0)
476                    {
477                        try {
478                           CqAttachment attachment = ViewRecord.selectAttachment
479                               (frame, fields, table.getSelectedRow(), "Remove");
480                           
481                           if (attachment != null)
482                               attachment.doUnbindAll(null);
483                        } catch(Throwable t) 
484                            { Utilities.exception(frame, "Remove Attachment", t);}
485                        }
486                    }
487                );
488                
489                panel.add(new JScrollPane(table), BorderLayout.CENTER);
490                panel.add(buttons, BorderLayout.SOUTH);
491                frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
492                frame.setContentPane(panel);
493                frame.setBounds(300, 300, 600, 300);
494                frame.setVisible(true);
495                
496                return frame;
497            }
498        }
499        
500        /** The field meta-properties requested and displayed */
501        static PropertyRequestItem.NestedPropertyName<?>[] fieldMetaProperties 
502            = ViewRecord.fieldMetaProperties;
503                      
504        /** The record properties read prior to editing the record */
505        final static PropertyRequest RECORD_PROPERTIES =
506            new PropertyRequest(
507                CqRecord.USER_FRIENDLY_LOCATION,
508                CqRecord.STABLE_LOCATION,
509                CqRecord.LEGAL_ACTIONS.nest(
510                    CqAction.USER_FRIENDLY_LOCATION,
511                    CqAction.DISPLAY_NAME,
512                    CqAction.TYPE),
513                CqRecord.ALL_FIELD_VALUES.nest(
514                    StpProperty.VALUE.nest(fieldMetaProperties))
515            );
516        
517        /**
518         * Extends the ViewRecord example by adding buttons on the record view
519         * window to start an edit action, add an attachment, or remove an 
520         * attachment
521         * @param args not used
522         */
523        public static void main(
524            String[] args)
525        {
526            try {
527                CqProvider provider = Utilities.getProvider().cqProvider();
528                ExecuteQuery.run("Edit Record", provider, new Viewer(provider));
529            } catch(Throwable ex) {
530                Utilities.exception(null, "Edit Record", ex);
531                System.exit(0);
532            }
533        }
534        
535        /**
536         * Determines if the given PropertyName's have the same root name and
537         * namespace (ignoring any nested properties they may have).
538         * @param n1 A PropertyName. Must not be null
539         * @param n2 Another PropertyName. Must not be null
540         * @return true if n1 and n2 have the same root property name.
541         */
542        static boolean matches(PropertyName n1, PropertyName n2)
543        {
544            String ns = n1.getNamespace();
545            
546            return n1.getName().equals(n2.getName()) &&
547                (ns == null? n2.getNamespace()==null: ns.equals(n2.getNamespace()));
548        }
549    }