Die Methode view(CqRecord) liest jetzt zusätzlich zur Eigenschaft ALL_FIELD_VALUES auch die Eigenschaft LEGAL_ACTIONS des Datensatzes. Der Wert der Eigenschaft LEGAL_ACTIONS ist eine Liste von Action-Proxys für die Aktionen, die für die Anwendung auf den Datensatz in seinem aktuellen Status gültig sind. Diese Liste wird verwendet, um das kombinierte Feld zu füllen, aus dem die Benutzer eine Aktion auswählen können, bevor Sie auf die Editierschaltfläche ("Edit") klicken. Einige Typen von Aktionen, z. B. SUBMIT und RECORD_SCRIPT_ALIAS, werden von diesem Beispiel nicht unterstützt, und wurden daher nicht zum kombinierten Feld hinzugefügt, selbst wenn sie in der Gruppe der gültigen Aktionen enthalten sind.
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 unterstützt auch die Schaltflächen Attach (Anhängen) und Detach (Abhängen) zum Hinzufügen und Entfernen der angehängten Dateien eines Anhangsfelds.
Die Schaltfläche Attach präsentiert dem Benutzer einen Swing-Standarddialog für Dateiauswahl und erlaubt ihm die Auswahl der Datei, die angehängt werden soll. Zum Anhängen der Datei an den Datensatz wird die Methode CqAttachment.doCreateAttachment() verwendet, an die der Name der vom Benutzer ausgewählten Datei übergeben wird. Damit diese Methode aufgerufen werden kann, muss zunächst ein CqAttachment-Proxy erstellt werden, der die Adresse der richtigen Speicherposition angibt. Der Ordner, in dem die Anhangsresssource enthalten sein soll, ist der Wert des Feldes, an das sie angehängt wird. Dies ist das Feld, das derzeit in der Datensatzansicht ausgewählt ist, folglich wird der Wert aus dem von ViewRecord.showRecord zurückgegebenen Frame-Objekt abgerufen. Für die Anhangsressource wird aus der aktuellen Uhrzeit ein eindeutiger Name generiert. Dies ist lediglich ein Platzhalter, weil die Methode doCreateAttachment() den Namen der Ressource nach Bedarf ändern kann.
add.addActionListener(new ActionListener(){ /** * Fordert den Benutzer zur Angabe des Namens einer Datei auf, * die an das ausgewählte Feld angehängt werden soll. * Wenn dies angegeben wird, wird der Inhalt der Datei als * Anhang in die ClearQuest-Datenbank gelesen. */ 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);} }}});
Die Schaltfläche Detach verwendet ViewRecord.selectAttachment, um vom Benutzer die Identität des Anhangs, der entfernt werden soll, in Form eines CqAttachment-Proxy anzufordern. Anschließend wird die Methode doUnbindAll dieses Proxy aufgerufen, um den Anhang aus der Datenbank zu entfernen.
remove.addActionListener(new ActionListener(){ /** * Erlaubt dem Benutzer die Auswahl eines mit dem ausgewählten * Feld verknüpften Anhangs. Wenn der Benutzer einen solchen * Anhang auswählt, wird dieser gelöscht. */ 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);} } } );
Die Schaltfläche Edit (Bearbeiten) ruft den ausgewählten Proxy CqAction aus dem kombinierten Feld ab und übergibt ihn sowie den Datensatz-Proxy für den aktuellen Viewer an die Bearbeitungsmethode, die dann die Bearbeitung des Datensatzes tatsächlich einleitet. Wenn der CqAction.Type der ausgewählten Aktion DUPLICATE lautet, muss für die ID des duplizierten Datensatzes die Aktion angegeben werden. Dieser Wert wird vom Benutzer angefordert und in die Argumentenzuordnung für "Action" als Wert des Arguments original übernommen.
start.addActionListener(new ActionListener(){ /** * Die Bearbeitung des Datensatzes wird mit der derzeit im * kombinierten Feld des Ansichtsdialogfensters ausgewählten * Aktion gestartet. */ public void actionPerformed(ActionEvent arg0) { CqAction action = (CqAction)choices.getSelectedItem(); try { if (action.getType() == CqAction.Type.DUPLICATE) { String id = JOptionPane.showInputDialog (frame, "ID des duplizierten Datensatzes angeben"); 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; }
Die Bearbeitungsmethode wird aufgerufen, wenn der Benutzer im Datensatz-Viewer auf die Schaltfläche Edit klickt. Ein Proxy für den zu bearbeitenden Datensatz und ein Proxy für die Aktion, unter der die Bearbeitung ausgeführt wird, werden übergeben.
Beachten Sie, dass für den Action-Proxy, der zum Starten einer ClearQuest-Aktion verwendet wird, keine Eigenschaften definiert werden müssen - lediglich seine Position und (falls erforderlich) die Argumentenzuordnung müssen definiert werden. Wenn Sie die Position einer Aktion festlegen möchten, müssen ihr <Name>, der <Satztyp> des Datensatzes, mit dem die Aktion verwendet werden soll, sowie <Datenbank> und <Datenbankgruppe>, in denen sich der Datensatz befindet, bekannt sein.
Die Position lautet dann wie folgt: cq.action:<Satztyp>/<Name>@<Datenbankgruppe>/<Datenbank>. Ein Beispiel hierfür sieht wie folgt aus: cq.action:Defect/Assign@7.0.0/SAMPL
Unter Verwendung eines neuen Proxy wird CqRecord.doWriteProperties aufgerufen, um die Bearbeitungsoperation zu starten. Als einzige Eigenschaft muss die Aktion angegeben werden, unter der der Datensatz bearbeitet werden soll. Die Methode doWriteProperties gibt einen Proxy für den Datensatz zurück, der zur Bearbeitung geöffnet wurde, und dieser Proxy wird die Eigenschaften enthalten, die in der Eigenschaftsanforderung (PropertyRequest) RECORD_PROPERTIES angefordert wurden. Die Eigenschaften könnten auch (mit derselben Eigenschaftsanforderung (PropertyRequest)) über record.doReadProperties nach der Ausführung von doWriteProperties abgerufen werden, dies wäre jedoch weniger effizient und würde einen weiteren Roundtrip an das Repository für dieselben Daten erfordern.
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 ist ähnlich wie showRecord. Der Hauptunterschied besteht darin, dass diese Methode TableModel.isCellEditable und TableModel.setValue definiert.
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 gibt nur dann "true" für die Spalte "VALUE" zurück, wenn die Zeile zu einem Feld gehört, in dem "REQUIREDNESS" nicht auf "READ_ONLY" gesetzt ist.
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 legt den neuen Feldwert im CqRecord-Proxy fest, der mit der Anzeige verknüpft ist. Zunächst wird die Struktur von CqFieldValue mittels seiner Methode initialize() aktualisiert. Diese Methode akzeptiert eine Zeichenfolgedarstellung für die meisten Feldtypen und wandelt die Zeichenfolge in den geeigneten Datentyp für das Feld um.
Nachdem CqFieldValue aktualisiert wurde, wird es in dem CqRecord-Proxy festgelegt, der dem PropertyName (Eigenschaftsnamen) für das Feld zugeordnet ist. Dieser Schritt ist erforderlich, damit beim Festschreiben des Datensatzes der neue Feldwert in die Datenbank geschrieben wird. Ohne diesen Schritt würde der neue Feldwert nur in dem Objekt CqFieldValue verbleiben, das Teil der Eigenschaft ALL_FIELD_VALUES ist. In die Eigenschaft ALL_FIELD_VALUES kann nicht geschrieben werden, daher würde die Änderung niemals in die Datenbank geschrieben werden. Indem der geänderte Wert CqFieldValue direkt in den Proxy-Eintrag für das Feld geschrieben wird, wird dieses Feld zu einer aktualisierten Eigenschaft, und der aktualisierte Wert wird von der nächsten do-Methode, die für den Proxy ausgeführt wird, in die Datenbank geschrieben.
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); // Anfordern, dass bei Änderung der Auswahl eine Benachrichtigung erfolgt. ListSelectionModel rowSM = table.getSelectionModel(); rowSM.addListSelectionListener(new ListSelectionListener() { /** * Aktiviert die Schaltflächen im Dialog abhängig vom * Typ des in der Tabelle ausgewählten Feldes. */ 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); } } } });
Die Schaltfläche View (Anzeigen) entspricht der Schaltfläche View in einem ViewRecord.Viewer.
buttons.add(show); show.addActionListener(new ActionListener(){ /** * Ruft für den Wert des in der Tabelle ausgewählten * Felds eine Ansichtsmethode auf. */ 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")); } } } });
Mit der Schaltfläche Deliver (Übergeben) wird der geänderte Datensatz in der Datenbank festgeschrieben, indem die Methode doWriteProperties() mit der Option CqProvider.DELIVER aufgerufen wird. Nach erfolgreicher Übergabe wird der Viewer geschlossen, und ein neuer Viewer wird für den ursprünglichen Proxy aufgerufen. Weil die Methode view() die Eigenschaften aus der Datenbank erneut liest, zeigt diese neue Ansicht die aktualisierten Werte an.
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); } } });
Mit der Schaltfläche Cancel (Abbrechen) wird die Bearbeitungsoperation verlassen, indem die Methode doRevert des Datensatz-Proxy ausgeführt wird. Wie bei Verwendung der Schaltfläche Deliver wird der Viewer geschlossen, und ein neuer Viewer wird für den ursprünglichen Proxy instanziiert.
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); } } });
Die Schaltflächen Attach und Detach entsprechen denen im ViewRecord.Viewer, jedoch lautet das Argument für die Übergabereihenfolge HOLD anstatt DELIVER. Dadurch wird das Hinzufügen oder Löschen des Anhangs so lange verzögert, bis der gesamte Datensatz über die Schaltfläche Deliver festgeschrieben wird.
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; }
Die grafische Benutzerschnittstelle könnte zwar jedesmal, wenn ein Feld geändert wird, record.doWriteProperties aufrufen, so dass der Benutzer ein unmittelbares Feedback erhalten würde, aber dies könnte je nach Übertragungsprotokoll zwischen Client und Server auch eine äußerst ineffiziente Vorgehensweise sein. Die grafische Benutzerschnittstelle könnte außerdem die Schaltfläche Update (Aktualisieren) bereitstellen, die bewirken würde, dass alle aufgelaufenen Aktualisierungen auf den Server geschrieben werden, ohne diese tatsächlich festzuschreiben. Auf diese Weise hätte das Schema die Möglichkeit, die Werte zu prüfen und Fehler zurückzumelden. Welche dieser Änderungen umgesetzt wird, bleibt Sache des Lesers, und die gewählte Strategie wäre abhängig von der vorgesehenen Verwendung dieser Anwendung.