view(CqRecord) 메소드는 이제 ALL_FIELD_VALUES 특성 외에 레코드의 LEGAL_ACTIONS 특성을 읽습니다. LEGAL_ACTIONS 특성의 값은 현재 상태에 있는 레코드에 정식으로 적용될 수 있는 조치에 대한 Action 프록시의 목록입니다. 이 목록은 사용자가 편집 단추를 클릭하기 전에 조치를 선택할 수 있는 콤보 상자 제어를 채우는 데 사용됩니다. 일부 조치 유형(예: SUBMIT 및 RECORD_SCRIPT_ALIAS)은 이 예제에서 지원되지 않으므로, 정식 조치 세트에 있어도 콤보 상자 제어에 추가되지 않습니다.
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});
EditView.Viewer는 또한 첨부 필드의 첨부된 파일 추가 및 제거를 위한 첨부 및 분리 단추를 지원합니다.
첨부 단추는 사용자에게 표준 Swing 파일 선택기 대화 상자를 제시하여 첨부할 파일을 선택할 수 있도록 합니다. 파일을 레코드에 첨부하기 위해 CqAttachment.doCreateAttachment()가 사용되어, 사용자가 선택한 파일의 이름을 전달합니다. 이 메소드를 호출하려면 먼저 적절한 위치를 주소 지정하는 CqAttachment 프록시를 생성해야 합니다. 첨부 자원이 상주할 폴더는 첨부되는 필드의 값입니다. 이는 현재 레코드 뷰에서 선택된 필드이므로 ViewRecord.showRecord에서 리턴된 프레임 오브젝트에서 확보할 수 있습니다. 첨부 자원에 대한 고유 이름은 현재 시간으로 생성됩니다. 이 이름은 단지 플레이스홀더입니다. doCreateAttachment()가 필요에 따라 자원의 이름을 자유롭게 변경할 수 있기 때문입니다.
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 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);} }}});
분리 단추는 ViewRecord.selectAttachment를 사용하여 CqAttachment 프록시의 양식에서 제거될 첨부의 ID를 가져옵니다. 이 프록시의 doUnbindAll 메소드는 데이터베이스에서 첨부를 제거하기 위해 호출됩니다.
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);} } } );
편집 단추는 콤보 상자에서 선택된 CqAction 프록시를 페치하고 레코드 편집을 실제로 초기화할 edit 메소드로 현재 뷰어에 대한 레코드 프록시와 이 프록시를 전달합니다. 선택된 조치의 CqAction.Type이 DUPLICATE인 경우 중복된 레코드의 id를 조치와 함께 제공해야 합니다. 이 값은 사용자로부터 요청되며 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; }
edit 메소드는 사용자가 레코드 뷰어에서 편집 단추를 클릭할 때 호출됩니다. 이 메소드는 편집할 레코드에 대한 프록시와 편집이 수행될 조치에 대한 프록시를 전달합니다.
ClearQuest® 조치를 시작하기 위해 사용되는 Action 프록시에 의해 어떤 특성도 정의되지 않아도 됩니다. 위치 및 인수 맵(필요한 경우)만 정의하면 됩니다. 조치에 대한 위치를 형성하려면 해당되는 <name>, 함께 사용될 레코드의 <record-type>, 레코드가 있는 <database> 및 <db-set>를 알아야 합니다.
위치는 cq.action:<record-type>/<name>@<db-set>/<database>입니다(예: cq.action:Defect/Assign@7.0.0/SAMPL).
새 프록시를 사용하여 CqRecord.doWriteProperties를 호출하고 편집 오퍼레이션을 시작합니다. 작성될 유일한 특성은 레코드가 편집되는 조치입니다. doWriteProperties 메소드는 편집 시 열린 레코드에 대한 프록시를 리턴하며 이 프록시는 RECORD_PROPERTIES PropertyRequest에서 요청된 특성을 포함합니다. 특성은 doWriteProperties가 리턴한 후 record.doReadProperties를 사용하여(동일한 PropertyRequest와 함께) 확보할 수도 있지만, 동일한 데이터에 대한 저장소에 대해 다른 라운드트립이 필요하므로 덜 효율적입니다.
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는 showRecord와 유사합니다. 기본적인 차이점은 TableModel.isCellEditable 및 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은 VALUE 열에 대해서만, REQUIREDNESS가 READ_ONLY가 아닌 필드에 행이 속하는 경우에만 true를 리턴합니다.
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는 표시와 연관되는 CqRecord 프록시로 새 필드 값을 설정합니다. 우선, CqFieldValue 구조는 initialize() 메소드를 사용하여 업데이트됩니다. 이 메소드는 대부분의 유형의 필드에 대한 문자열 표시를 승인하고 문자열을 필드의 적절한 데이터 유형으로 변환합니다.
CqFieldValue가 업데이트되면 필드에 대한 PropertyName과 연관되는 CqRecord 프록시로 설정됩니다. 이 단계는 레코드를 확약할 때 새 필드 값을 데이터베이스에 쓰기 위해 필요합니다. 이 단계가 없으면, 새 필드 값은 ALL_FIELD_VALUES 특성의 일부인 CqFieldValue 오브젝트에서만 유지됩니다. ALL_FIELD_VALUES 특성은 쓸 수 없으므로, 변경사항은 데이터베이스에 쓰여지지 않습니다. 수정된 CqFieldValue를 필드의 프록시 항목으로 직접 복사하면 해당 필드는 업데이트된 특성이 되고 업데이트된 값은 프록시에 대해 실행되는 다음 do 메소드에 의해 데이터베이스에 쓰여집니다.
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); } } } });
보기 단추는 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")); } } } });
Deliver 단추는 CqProvider.DELIVER 옵션으로 doWriteProperties()를 호출하여 데이터베이스에 수정된 레코드를 커미트합니다. 전달되고 나면, 뷰어는 분리되어 원래 프록시에서 새 Viewer를 가져옵니다. view() 메소드는 데이터베이스에서 특성을 다시 읽으므로, 새 보기는 업데이트된 값을 표시합니다.
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); } } });
취소 단추는 레코드 프록시의 doRevert 메소드를 사용한 편집 오퍼레이션을 포기합니다. 전달 단추와 같이, 뷰어가 닫히고 원래 프록시에 대한 새 뷰어가 인스턴스화됩니다.
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); } } });
첨부 및 분리 단추는 ViewRecord.Viewer에 대한 단추와 동일합니다. 단, 전달 순서 인수는 DELIVER가 아닌 HOLD입니다. 이는 전체 레코드가 전달 단추에 의해 커미트될 때까지 첨부 추가 또는 삭제 커미트를 지연합니다.
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; }
GUI는 필드가 수정될 때마다 record.doWriteProperties에 대한 호출을 수행할 수 있어서, 사용자에게 즉각적인 피드백을 제공하지만 클라이언트와 서버 사이의 통신 프로토콜에 따라 비효율적일 수 있습니다. GUI는 또한 실제로 전달하지 않고 누적된 모든 갱신사항이 서버에 기록되도록 하는 업데이트 단추를 제공합니다. 이 단추는 값을 조사하여 오류를 다시 보고할 수 있는 기회를 스키마에 제공합니다. 이 수정사항은 리더가 수행하도록 남겨지며 이 접근방식은 해당 애플리케이션의 의도한 용도에 따라 수행됩니다.