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 * (C) 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 */
015package com.ibm.rational.stp.client.samples;
016
017import java.awt.BorderLayout;
018import java.awt.FlowLayout;
019import java.awt.event.ActionEvent;
020import java.awt.event.ActionListener;
021import java.util.Hashtable;
022
023import javax.swing.JButton;
024import javax.swing.JComboBox;
025import javax.swing.JComponent;
026import javax.swing.JFileChooser;
027import javax.swing.JFrame;
028import javax.swing.JOptionPane;
029import javax.swing.JPanel;
030import javax.swing.JScrollPane;
031import javax.swing.JTable;
032import javax.swing.ListSelectionModel;
033import javax.swing.event.ListSelectionEvent;
034import javax.swing.event.ListSelectionListener;
035import javax.swing.table.AbstractTableModel;
036import javax.swing.table.TableModel;
037import javax.wvcm.Folder;
038import javax.wvcm.PropertyRequestItem;
039import javax.wvcm.WvcmException;
040import javax.wvcm.PropertyNameList.PropertyName;
041import javax.wvcm.PropertyRequestItem.PropertyRequest;
042
043import com.ibm.rational.wvcm.stp.StpException;
044import com.ibm.rational.wvcm.stp.StpLocation;
045import com.ibm.rational.wvcm.stp.StpProperty;
046import com.ibm.rational.wvcm.stp.StpProperty.MetaPropertyName;
047import com.ibm.rational.wvcm.stp.cq.CqAction;
048import com.ibm.rational.wvcm.stp.cq.CqAttachment;
049import com.ibm.rational.wvcm.stp.cq.CqFieldDefinition;
050import com.ibm.rational.wvcm.stp.cq.CqFieldValue;
051import com.ibm.rational.wvcm.stp.cq.CqProvider;
052import 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 */
060public 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}