package org.openslx.dozmod.gui.window; import java.awt.Color; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.swing.DefaultComboBoxModel; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.apache.log4j.Logger; import org.apache.thrift.TException; import org.openslx.bwlp.thrift.iface.ImageDetailsRead; import org.openslx.bwlp.thrift.iface.ImageSummaryRead; import org.openslx.bwlp.thrift.iface.ImageVersionDetails; import org.openslx.bwlp.thrift.iface.LecturePermissions; import org.openslx.bwlp.thrift.iface.LectureRead; import org.openslx.bwlp.thrift.iface.LectureWrite; import org.openslx.bwlp.thrift.iface.UserInfo; import org.openslx.dozmod.gui.Gui; import org.openslx.dozmod.gui.MainWindow; import org.openslx.dozmod.gui.helper.DateTimeHelper; import org.openslx.dozmod.gui.helper.MessageType; import org.openslx.dozmod.gui.helper.UiFeedback; import org.openslx.dozmod.gui.window.UserListWindow.UserAddedCallback; import org.openslx.dozmod.gui.window.layout.LectureDetailsWindowLayout; import org.openslx.dozmod.permissions.ImagePerms; import org.openslx.dozmod.permissions.LecturePerms; import org.openslx.dozmod.thrift.Session; import org.openslx.dozmod.thrift.ThriftActions; import org.openslx.dozmod.thrift.ThriftActions.DownloadCallback; import org.openslx.dozmod.thrift.ThriftActions.LectureMetaCallback; import org.openslx.dozmod.thrift.ThriftError; import org.openslx.dozmod.thrift.cache.UserCache; import org.openslx.dozmod.util.FormatHelper; import org.openslx.dozmod.util.MapHelper; import org.openslx.thrifthelper.ThriftManager; /** * Window to display and edit the details of a lecture */ @SuppressWarnings("serial") public class LectureDetailsWindow extends LectureDetailsWindowLayout implements UiFeedback { private static final Logger LOGGER = Logger.getLogger(LectureDetailsWindow.class); /** * Self-reference */ private final LectureDetailsWindow me = this; /** * Callback interface to refresh lecture list after changing lecture details */ public interface LectureUpdatedCallback { public void updated(boolean success); } /** * Callback to be called when changing lecture details on the server */ private LectureUpdatedCallback callback = null; /** * Lecture that this window shows the details of */ private LectureRead lecture = null; /** * The custom permissions of the lecture */ private Map customPermissions; /** * The original custom permissions as fetched from the server */ private Map originalCustomPermissions; /** * The original default permissions as fetched from the server */ private LecturePermissions originalDefaultPermissions; /** * Image, that the lecture is linked to. */ private ImageDetailsRead image = null; private boolean imageLinkChanged = false; private boolean metadataChanged = false; private boolean permissionsChanged; /** * Constructor * * @param modalParent parent of this popup window */ public LectureDetailsWindow(Frame modalParent, LectureUpdatedCallback callback) { super(modalParent); // save callback this.callback = callback; setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { safeClose(); } }); btnLinkImage.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { ImageSummaryRead newImage = LectureChangeImage.open(me); if (newImage != null) { try { image = ThriftManager.getSatClient().getImageDetails(Session.getSatelliteToken(), newImage.imageBaseId); } catch (TException e1) { LOGGER.error("Failed to retrieve details of new image: ", e1); return; } lecture.imageBaseId = newImage.imageBaseId; lecture.imageVersionId = newImage.latestVersionId; fillVersionsCombo(); cboVersions.setEnabled(false); chkAutoUpdate.setSelected(true); imageLinkChanged = true; txtImageName.setText(newImage.getImageName()); reactToChange(); } } }); btnClose.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { safeClose(); } }); btnDownloadImage.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { performImageDownload(); } }); chkAutoUpdate.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { cboVersions.setEnabled(!chkAutoUpdate.isSelected()); } }); btnChangeOwner.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { UserListWindow.open(me, new UserAddedCallback() { @Override public void userAdded(UserInfo user, UserListWindow window) { window.dispose(); if (Gui.showMessageBox(me, "Sind Sie sicher, dass sie die Besitzerrechte an " + "einen anderen Account übertragen wollen?", MessageType.QUESTION_YESNO, LOGGER, null)) setLectureOwner(user); } }, "Besitzer festlegen", lecture.ownerId); } }); btnPermissions.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { // NOTE once the following window is closed, customPermissions and lecture.defaultPermissions // objects will contain the changes the user did. We will later only compare these with // the original values as saved in originalCustomPermissions and originalDefaultPermissions LecturePermissionWindow.open(me, customPermissions, lecture.defaultPermissions, lecture.ownerId); reactToChange(); } }); btnSaveChanges.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { saveChanges(); } }); // final step, add listeners to react to change final DocumentListener docListener = new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { changedUpdate(e); } @Override public void insertUpdate(DocumentEvent e) { changedUpdate(e); } @Override public void changedUpdate(DocumentEvent e) { reactToChange(); } }; txtTitle.getDocument().addDocumentListener(docListener); txtDescription.getDocument().addDocumentListener(docListener); // Comboboxes final ItemListener comboItemListener = new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { reactToChange(); } } }; cboVersions.addItemListener(comboItemListener); // Listener to detect changes in checkboxes final ActionListener actionListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { reactToChange(); } }; ChangeListener changeListener = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { reactToChange(); } }; chkAutoUpdate.addActionListener(actionListener); chkIsExam.addActionListener(actionListener); chkHasInternetAccess.addActionListener(actionListener); chkIsActive.addActionListener(actionListener); startDate.addActionListener(actionListener); endDate.addActionListener(actionListener); startTime.addChangeListener(changeListener); endTime.addChangeListener(changeListener); // save default color of date/time stuff to reset the background later dateTimeTextColor = startDate.getForeground(); // disable save button btnSaveChanges.setEnabled(false); // wait for the image to be loaded before (potentially) enabling fields makeEditable(false); } /** * Sets the lecture to show the details of by setting the 'lecture' and * 'image' * members to its metadata. This method will fetch the information from the * sat * * @param lectureId the id of the lecture to be displayed */ public void setLecture(final String lectureId) { ThriftActions.getLectureAndImageDetails(JOptionPane.getFrameForComponent(me), lectureId, new LectureMetaCallback() { @Override public void fetchedLectureAndImageDetails(LectureRead lectureDetails, ImageDetailsRead imageDetails) { synchronized (me) { lecture = lectureDetails; image = imageDetails; if (lecture != null) { customPermissions = ThriftActions.getLecturePermissions( JOptionPane.getFrameForComponent(me), lecture.lectureId); } } fillDetails(); } }); } /** * callback function when we received the lecture's details from the server */ private void fillDetails() { if (lecture == null) { txtTitle.setText("-"); makeEditable(false); return; } if (image == null) { txtImageName.setText("-"); } else { txtImageName.setText(image.getImageName()); } // remember default permissions if (lecture.defaultPermissions != null) { originalDefaultPermissions = new LecturePermissions(lecture.defaultPermissions); } // remember custom permissions if (customPermissions != null) { // need a deep copy of the permission map to be able to check for changes after ImageCustomPermissionWindow if (originalCustomPermissions == null) originalCustomPermissions = new HashMap(); else originalCustomPermissions.clear(); // fill it for (Entry entry : customPermissions.entrySet()) { originalCustomPermissions.put(entry.getKey(), new LecturePermissions(entry.getValue())); } } txtTitle.setText(lecture.getLectureName()); txtDescription.setText(lecture.getDescription()); lblOwner.setUser(UserCache.find(lecture.getOwnerId())); lblUpdater.setUser(UserCache.find(lecture.getUpdaterId())); lblCreateTime.setText(FormatHelper.longDate(lecture.getCreateTime())); lblUpdateTime.setText(FormatHelper.longDate(lecture.getUpdateTime())); lblStartTime.setText(FormatHelper.longDate(lecture.getStartTime())); lblEndTime.setText(FormatHelper.longDate(lecture.getEndTime())); txtId.setText(lecture.getLectureId()); chkIsActive.setSelected(lecture.isEnabled); chkHasInternetAccess.setSelected(lecture.hasInternetAccess); chkIsExam.setSelected(lecture.isExam); chkAutoUpdate.setSelected(lecture.autoUpdate); cboVersions.setEnabled(!lecture.autoUpdate); lblUseCount.setText(Integer.toString(lecture.useCount)); fillVersionsCombo(); Calendar startCal = Calendar.getInstance(); startCal.setTime(new Date(lecture.getStartTime() * 1000l)); startDate.getModel().setDate(startCal.get(Calendar.YEAR), startCal.get(Calendar.MONTH), startCal.get(Calendar.DATE)); startTime.getModel().setValue(startCal.getTime()); Calendar endCal = Calendar.getInstance(); endCal.setTime(new Date(lecture.getEndTime() * 1000l)); endDate.getModel().setDate(endCal.get(Calendar.YEAR), endCal.get(Calendar.MONTH), endCal.get(Calendar.DATE)); endTime.getModel().setValue(endCal.getTime()); makeEditable(true); setVisible(true); } /** * Helper to fill the combobox with the versions of the image. The list will * be sorted by creation timestamp */ private void fillVersionsCombo() { List versions; if (image == null) { versions = new ArrayList<>(0); } else { versions = image.getVersions(); } // version combo for (Iterator it = versions.iterator(); it.hasNext();) { ImageVersionDetails version = it.next(); if (!version.isValid && !lecture.imageVersionId.equals(version.versionId)) { it.remove(); } } // sort versions by createtime Collections.sort(versions, new Comparator() { public int compare(ImageVersionDetails o1, ImageVersionDetails o2) { return -Long.compare(o1.getCreateTime(), o2.getCreateTime()); } }); cboVersions.setModel(new DefaultComboBoxModel( versions.toArray(new ImageVersionDetails[versions.size()]))); cboVersions.setSelectedItem(new ImageVersionDetails(lecture.getImageVersionId(), 0, 0, 0, null, true, true, true, null)); } /** * Sets the lecture's owner to the given user * * @param user UserInfo representation of the new owner */ private void setLectureOwner(final UserInfo user) { if (!ThriftActions.setLectureOwner(JOptionPane.getFrameForComponent(this), lecture.getLectureId(), user)) { return; } // success Gui.showMessageBox(me, "Besitzrechte übertragen an " + FormatHelper.userName(user), MessageType.INFO, null, null); makeEditable(false); String lectureId = lecture.getLectureId(); synchronized (me) { image = null; lecture = null; } setLecture(lectureId); } /** * Triggers the download of the currently used image version of the lecture */ private void performImageDownload() { if (image == null) { Gui.showMessageBox(this, "Image ungültig.", MessageType.ERROR, null, null); return; } btnDownloadImage.setEnabled(false); long versionSize = 0; for (ImageVersionDetails version : image.versions) { if (version.versionId.equals(lecture.imageVersionId)) { if (!version.isValid) { Gui.showMessageBox(this, "Ungültiges Image gewählt", MessageType.ERROR, null, null); return; } versionSize = version.fileSize; break; } } if (versionSize == 0) { Gui.showMessageBox(this, "Fehler bei der Abfrage der Größe des Images.", MessageType.ERROR, null, null); return; } ThriftActions.initDownload(JOptionPane.getFrameForComponent(this), lecture.imageVersionId, image.imageName, image.virtId, image.osId, versionSize, new DownloadCallback() { @Override public void downloadInitialized(boolean success) { if (!success) { Gui.asyncExec(new Runnable() { @Override public void run() { btnDownloadImage.setEnabled(true); } }); } } }); } /** * Push the changes of the image details to the satellite */ private void saveChanges() { boolean saved = saveChangesInternal(); callback.updated(saved); if (saved) dispose(); else btnSaveChanges.setEnabled(true); } private boolean saveChangesInternal() { // check, whether autoupdate is selected and choose version accordingly if (image != null) { lecture.imageVersionId = chkAutoUpdate.isSelected() ? image.latestVersionId : cboVersions.getItemAt(cboVersions.getSelectedIndex()).versionId; } // now check if we need to push a new LectureWrite if (metadataChanged) { // first build the LectureWrite from the GUI fields final LectureWrite metadata = new LectureWrite(txtTitle.getText(), txtDescription.getText(), lecture.getImageVersionId(), chkAutoUpdate.isSelected(), chkIsActive.isSelected(), DateTimeHelper.getDateFrom(startDate, startTime).getTime() / 1000L, DateTimeHelper.getDateFrom(endDate, endTime).getTime() / 1000L, null, null, chkIsExam.isSelected(), chkHasInternetAccess.isSelected(), lecture.getDefaultPermissions()); // now trigger the actual action try { ThriftManager.getSatClient().updateLecture(Session.getSatelliteToken(), lecture.getLectureId(), metadata); metadataChanged = false; LOGGER.info("Successfully save new metadata"); } catch (TException e) { ThriftError.showMessage(JOptionPane.getFrameForComponent(this), LOGGER, e, "Fehler beim Updaten der Veranstaltung!"); callback.updated(false); return false; } } if (permissionsChanged) { try { ThriftManager.getSatClient().writeLecturePermissions(Session.getSatelliteToken(), lecture.lectureId, customPermissions); permissionsChanged = false; LOGGER.info("Successfully save new permissions"); } catch (TException e) { ThriftError.showMessage(JOptionPane.getFrameForComponent(this), LOGGER, e, "Fehler beim Übertragen der Berechtigungen!"); callback.updated(true); return false; } } return true; } /** * Checks if the given start and end date represent a valid time period. * This is the case, if start < end and if current time < end * * @param start date of the period to check * @param end date of the period to check * @param feedback true if the user should be shown feedback, false * otherwise * @return true if the period is valid, false otherwise */ private boolean isPeriodValid(final Date start, final Date end, boolean feedback) { if (start == null || end == null) return false; // analyse time stuff to see if its valid if (start.after(end)) { startDate.setForeground(Color.red); if (feedback) Gui.showMessageBox(me, "Start der Veranstaltung ist nach dem Enddatum!", MessageType.ERROR, LOGGER, null); } else { startDate.setForeground(dateTimeTextColor); final Date now = new Date(); if (now.after(end)) { if (feedback) Gui.showMessageBox(me, "Enddatum liegt in der Vergangenheit!", MessageType.ERROR, LOGGER, null); endDate.setForeground(Color.red); } else { endDate.setForeground(dateTimeTextColor); return true; } } return false; } /** * Check whether the date has been changed * * @return true if it has changed, false otherwise */ private boolean dateHasChanged() { return (DateTimeHelper.getDateFrom(startDate, startTime).getTime() / 1000L) != lecture.getStartTime() || (DateTimeHelper.getDateFrom(endDate, endTime).getTime() / 1000L) != lecture.getEndTime(); } /** * Checks whether the user changed any fields of the image details and * enables the save button if so. */ private void reactToChange() { // check details fields metadataChanged = reactToChangeInternal(); permissionsChanged = MapHelper.hasChanged(originalCustomPermissions, customPermissions); btnSaveChanges.setEnabled(metadataChanged || permissionsChanged); } /** * Checks whether the user changed any fields of the image details and * enables the save button if so. */ private boolean reactToChangeInternal() { if (lecture == null) return false; boolean changed = false; // mandatory fields checks if (txtTitle.getText().isEmpty()) { lblError.setText("Kein Imagename!"); return false; } if (txtDescription.getText().isEmpty()) { lblError.setText("Keine Beschreibung!"); return false; } // version checkbox changed? ImageVersionDetails currentVersion = cboVersions.getItemAt(cboVersions.getSelectedIndex()); if (currentVersion == null) { lblError.setText("Keine Version ausgewählt!"); return false; } // Date stuff Date start = DateTimeHelper.getDateFrom(startDate, startTime); Date end = DateTimeHelper.getDateFrom(endDate, endTime); if (!isPeriodValid(start, end, false) && dateHasChanged()) { lblError.setText("Ungültiger Zeitraum!"); return false; } // done with mandatory checks, remove error message lblError.setText(null); // check for changes in all fields changed = !txtTitle.getText().equals(lecture.getLectureName()) || !txtDescription.getText().equals(lecture.getDescription()) || !currentVersion.getVersionId().equals(lecture.getImageVersionId()) || (DateTimeHelper.getDateFrom(startDate, startTime).getTime() / 1000L) != lecture.getStartTime() || (DateTimeHelper.getDateFrom(endDate, endTime).getTime() / 1000L) != lecture.getEndTime() || chkAutoUpdate.isSelected() != lecture.autoUpdate || chkIsExam.isSelected() != lecture.isExam || chkHasInternetAccess.isSelected() != lecture.hasInternetAccess || chkIsActive.isSelected() != lecture.isEnabled || !lecture.defaultPermissions.equals(originalDefaultPermissions) || imageLinkChanged; return changed; } /** * Enables/disables the editable fields based on 'editable' * * @param editable true to make fields editable, false otherwise. */ private void makeEditable(boolean editable) { editable = editable && (LecturePerms.canEdit(lecture) || LecturePerms.canAdmin(lecture)); txtTitle.setEditable(editable); txtDescription.setEditable(editable); btnLinkImage.setEnabled(editable); chkIsExam.setEnabled(editable); chkHasInternetAccess.setEnabled(editable); chkIsActive.setEnabled(editable); chkAutoUpdate.setEnabled(editable); cboVersions.setEnabled(editable && !lecture.autoUpdate && image != null); btnChangeOwner.setEnabled(editable && LecturePerms.canAdmin(lecture)); btnPermissions.setEnabled(editable && LecturePerms.canAdmin(lecture)); startDate.setEnabled(editable); startTime.setEnabled(editable); endDate.setEnabled(editable); endTime.setEnabled(editable); btnDownloadImage.setEnabled(ImagePerms.canDownload(image)); } /** * Opens a new LectureDetailsWindow showing the details of the * lecture with ID = lectureId * * @param modalParent parent of this window * @param lectureId id of the lecture to set the details of */ public static void open(Frame modalParent, String lectureId, LectureUpdatedCallback callback) { LectureDetailsWindow win = new LectureDetailsWindow(modalParent, callback); win.setLecture(lectureId); win.setVisible(true); } @SuppressWarnings("deprecation") @Override public void show() { if (!isVisible()) { pack(); MainWindow.centerShell(this); } super.show(); } /* ******************************************************************************* * * UIFeedback implementation * ********************************************************************************/ @Override public boolean wantConfirmQuit() { return metadataChanged || permissionsChanged; } @Override public void escapePressed() { // Also ask if applicable safeClose(); } /* * Safe close helper: checks if we have unsaved work and prompt the user for * confirmation if so */ private void safeClose() { if ((metadataChanged || permissionsChanged) && !Gui.showMessageBox(me, "Änderungen werden verworfen, wollen Sie wirklich schließen?", MessageType.QUESTION_YESNO, null, null)) return; dispose(); } }