package org.openslx.dozmod.gui.window; 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.Iterator; import java.util.List; import java.util.Map; import javax.swing.DefaultComboBoxModel; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; import org.apache.log4j.Logger; import org.apache.thrift.TException; import org.openslx.bwlp.thrift.iface.ImageDetailsRead; import org.openslx.bwlp.thrift.iface.ImagePermissions; 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.NetRule; import org.openslx.bwlp.thrift.iface.UserInfo; import org.openslx.dozmod.gui.Gui; import org.openslx.dozmod.gui.MainWindow; import org.openslx.dozmod.gui.changemonitor.AbstractControlWrapper; import org.openslx.dozmod.gui.changemonitor.DialogChangeMonitor; import org.openslx.dozmod.gui.changemonitor.DialogChangeMonitor.NotNullConstraint; import org.openslx.dozmod.gui.changemonitor.DialogChangeMonitor.TextNotEmptyConstraint; import org.openslx.dozmod.gui.changemonitor.DialogChangeMonitor.ValidationConstraint; 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.ImageLocalDetailsActions; 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.ImageMetaCallback; 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.thrifthelper.Comparators; 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; /** * Action handler proxying thrift calls for image related operations */ private final ImageLocalDetailsActions actionHandler; /** * Image, that the lecture is linked to. */ private ImageDetailsRead image = null; /** * Per-User permissions of this lecture */ private Map customPermissions; /** * Did the user have admin rights due to default permissions? */ private boolean adminRightsFromDefaultPermissions; private final DialogChangeMonitor changeMonitor; private final AbstractControlWrapper changeListenerPermissions; /** * Constructor * * @param modalParent parent of this popup window * @param callback function to be called when a lecture update occured */ public LectureDetailsWindow(Frame modalParent, LectureUpdatedCallback callback) { super(modalParent); // save callback this.callback = callback; setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // Set up change monitor changeMonitor = new DialogChangeMonitor(new DialogChangeMonitor.Callback() { @Override public void validityChanged(String errorMessage) { lblError.setText(errorMessage); btnSaveChanges.setEnabled(changeMonitor.isValid() && changeMonitor.wasEverModified()); LOGGER.info("Valid: " + changeMonitor.isValid()); } @Override public void modificationChanged() { btnSaveChanges.setEnabled(changeMonitor.isValid() && changeMonitor.wasEverModified()); LOGGER.info("Changed: " + changeMonitor.isCurrentlyModified()); } }); // Add controls to change monitor changeMonitor.addFixedCombo(cboVersions, Comparators.imageVersionDetails) .addConstraint( new DialogChangeMonitor.ValidationConstraint() { public String checkStateValid(ImageVersionDetails userInput) { if (userInput != null && userInput.isValid) return null; return "Keine/Ungültige VM-Version ausgewählt"; } }); // Create constraint for date start/end final Date maxValidity; Calendar c = Calendar.getInstance(); c.add(Calendar.DAY_OF_MONTH, Session.getSatelliteConfig().maxLectureValidityDays); maxValidity = c.getTime(); ValidationConstraint dateRangeValidator = new DialogChangeMonitor.ValidationConstraint() { public String checkStateValid(Object ignored) { Date start = DateTimeHelper.getDateFrom(dtpStartDate, spnStartTime); Date end = DateTimeHelper.getDateFrom(dtpEndDate, spnEndTime); if (!end.after(start)) return "Enddatum darf nicht vor dem Startdatum liegen"; if (end.after(maxValidity)) return "Enddatum liegt nach dem " + FormatHelper.shortDate(maxValidity); return null; } }; changeMonitor.add(chkAutoUpdate); changeMonitor.add(chkIsExam); changeMonitor.add(chkHasInternetAccess); changeMonitor.add(chkHasUsbAccess); changeMonitor.add(chkIsActive); changeMonitor.add(chkCustomPermAdmin); changeMonitor.add(chkCustomPermEdit); changeMonitor.add(txtTitle).addConstraint(new TextNotEmptyConstraint("Veranstaltungsname darf nicht leer sein")); changeMonitor.add(txtDescription).addConstraint(new TextNotEmptyConstraint("Beschreibung darf nicht leer sein")); changeMonitor.add(dtpEndDate).addConstraint(dateRangeValidator); changeMonitor.add(dtpStartDate).addConstraint(dateRangeValidator); changeMonitor.add(spnEndTime).addConstraint(dateRangeValidator); changeMonitor.add(spnStartTime).addConstraint(dateRangeValidator); changeMonitor.add(ctlNetrulesConfigurator).addConstraint(new NotNullConstraint>("Fehlerhafte Netzwerkregeln")); changeListenerPermissions = changeMonitor.add(ctlPermissionManager); ctlLocationSelector.addToChangeMonitor(changeMonitor); ctlRunscriptConfigurator.addToChangeMonitor(changeMonitor); ctlNetshareConfigurator.addToChangeMonitor(changeMonitor); ctlLdapFilterConfigurator.addToChangeMonitor(changeMonitor); // TODO: LDAP/NetShare: Having uncommitted changes in the input fields should be handled too // End change monitor addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { safeClose(); } }); actionHandler = new ImageLocalDetailsActions(JOptionPane.getFrameForComponent(LectureDetailsWindow.this)); btnLinkImage.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { final ImageSummaryRead newImage = LectureChangeImage.open(LectureDetailsWindow.this, changeMonitor); if (newImage == null) return; final ImageMetaCallback callback = new ImageMetaCallback() { @Override public void fetchedImageDetails(ImageDetailsRead imageDetails, Map permissions) { if (imageDetails == null) { return; } synchronized (me) { image = imageDetails; } lecture.imageBaseId = newImage.imageBaseId; lecture.imageVersionId = newImage.latestVersionId; fillVersionsCombo(); cboVersions.setEnabled(false); chkAutoUpdate.setSelected(true); txtImageName.setText(newImage.getImageName()); } }; actionHandler.getImageDetails(newImage.imageBaseId, callback); } }); btnClose.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { safeClose(); } }); btnDownloadImage.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { performImageDownload(); } }); chkAutoUpdate.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent 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); } }); // Update default permissions in the permission manager immediately, so it affects // newly added users final ItemListener updateDefaultPermissionListener = new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { if (e.getSource() == chkCustomPermAdmin) { lecture.defaultPermissions.admin = chkCustomPermAdmin.isSelected(); } else { lecture.defaultPermissions.edit = chkCustomPermEdit.isSelected(); } } }; chkCustomPermAdmin.addItemListener(updateDefaultPermissionListener); chkCustomPermEdit.addItemListener(updateDefaultPermissionListener); // last step, the save button btnSaveChanges.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { saveChanges(); } }); // disable save button btnSaveChanges.setEnabled(false); // wait for the image to be loaded before (potentially) enabling fields toggleEditable(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(); } } }); } /** * Internal callback function when we received the lecture's details from the server */ private void fillDetails() { if (lecture == null) { txtTitle.setText("-"); lblTitleInfo.setText("-"); toggleEditable(false); return; } if (image == null) { txtImageName.setText("-"); lblImageNameInfo.setText("-"); } else { txtImageName.setText(image.getImageName()); lblImageNameInfo.setText(image.getImageName()); } // init permission info adminRightsFromDefaultPermissions = lecture.defaultPermissions.admin; ctlPermissionManager.initPanel(customPermissions, lecture.defaultPermissions, lecture.ownerId); chkCustomPermAdmin.setSelected(lecture.defaultPermissions.admin); chkCustomPermEdit.setSelected(lecture.defaultPermissions.edit); // init location info ctlLocationSelector.setOnlyInSelection(lecture.limitToLocations); ctlLocationSelector.setSelectedLocationsAsIds(lecture.locationIds); // init advanced info ctlRunscriptConfigurator.setState(lecture.runscript); ctlNetshareConfigurator.setState(lecture.networkShares, lecture.presetNetworkShares); ctlLdapFilterConfigurator.setState(lecture.ldapFilters, lecture.presetLdapFilters); ctlNetrulesConfigurator.setState(lecture.networkExceptions); txtTitle.setText(lecture.getLectureName()); lblTitleInfo.setText(lecture.getLectureName()); txtDescription.setText(lecture.getDescription()); lblOwner.setUser(UserCache.find(lecture.getOwnerId())); lblOwnerInfo.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); chkHasUsbAccess.setSelected(lecture.hasUsbAccess); 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)); dtpStartDate.getModel().setDate(startCal.get(Calendar.YEAR), startCal.get(Calendar.MONTH), startCal.get(Calendar.DATE)); spnStartTime.getModel().setValue(startCal.getTime()); Calendar endCal = Calendar.getInstance(); endCal.setTime(new Date(lecture.getEndTime() * 1000l)); dtpEndDate.getModel().setDate(endCal.get(Calendar.YEAR), endCal.get(Calendar.MONTH), endCal.get(Calendar.DATE)); spnEndTime.getModel().setValue(endCal.getTime()); // now enable the tabs the user can see given its permissions toggleEditable(true); // and always switch to the "About" tab pnlTabs.setSelectedIndex(pnlTabs.indexOfTab("Übersicht")); setVisible(true); changeMonitor.reset(); } /** * 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); toggleEditable(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, "VM 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ültige VM-Version 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 VM-Abbildes.", 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); } }); } } }); } /** * Triggers the saving of the changes of this lecture * And inform the callback function about the outcome */ private void saveChanges() { boolean saved = saveChangesInternal(); callback.updated(saved); if (saved) { dispose(); } else { btnSaveChanges.setEnabled(true); } } /** * Internal helper actually saving the changes to the satellite * * @return true if saving succeeded, false otherwise. */ private boolean saveChangesInternal() { long startTime = DateTimeHelper.getDateFrom(dtpStartDate, spnStartTime).getTime() / 1000L; long endTime = DateTimeHelper.getDateFrom(dtpEndDate, spnEndTime).getTime() / 1000L; if (!isPeriodValid(startTime, endTime, true)) return false; // check, whether autoupdate is selected and choose version accordingly if (image != null) { lecture.imageVersionId = chkAutoUpdate.isSelected() ? image.latestVersionId : cboVersions.getItemAt(cboVersions.getSelectedIndex()).versionId; } // Special case similar to ImageDetailsWindow:if the user had admin rights // through default permissions, first save custom permissions. if (adminRightsFromDefaultPermissions && changeListenerPermissions.isCurrentlyChanged()) { if (!saveCustomPermissions()) { return false; } changeListenerPermissions.reset(); } // first build the LectureWrite from the GUI fields final LectureWrite metadata = new LectureWrite(txtTitle.getText(), txtDescription.getText(), lecture.getImageVersionId(), chkAutoUpdate.isSelected(), chkIsActive.isSelected(), startTime, endTime, ctlRunscriptConfigurator.getState(), null, chkIsExam.isSelected(), chkHasInternetAccess.isSelected(), lecture.getDefaultPermissions(), ctlLocationSelector.getSelectedLocationsAsIds(), ctlLocationSelector.getOnlyInSelection(), // TODO limitOnlyToAllowedUsers, default to false for now false, chkHasUsbAccess.isSelected()); metadata.setNetworkExceptions(ctlNetrulesConfigurator.getState()); metadata.setNetworkShares(ctlNetshareConfigurator.getState()); metadata.setLdapFilters(ctlLdapFilterConfigurator.getState()); // now trigger the actual action try { ThriftManager.getSatClient().updateLecture(Session.getSatelliteToken(), lecture.getLectureId(), metadata); LOGGER.info("Successfully saved new metadata"); } catch (TException e) { ThriftError.showMessage(JOptionPane.getFrameForComponent(this), LOGGER, e, "Fehler beim Speichern der Veranstaltung!"); return false; } if (changeListenerPermissions.isCurrentlyChanged()) { if (!saveCustomPermissions()) { return false; } } changeMonitor.reset(); return true; } /** * Helper to save custom user permissions. * * @return true if successfully saved, false otherwise. */ private boolean saveCustomPermissions() { try { ThriftManager.getSatClient().writeLecturePermissions(Session.getSatelliteToken(), lecture.lectureId, ctlPermissionManager.getPermissions()); LOGGER.info("Successfully saved custom permissions"); return true; } catch (TException e) { ThriftError.showMessage(JOptionPane.getFrameForComponent(this), LOGGER, e, "Fehler beim Übertragen der Berechtigungen!"); } return false; } /** * 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(long start, long end, boolean feedback) { if (start <= 0 || end <= 0) return false; // analyse time stuff to see if its valid if (end > start) return true; String msg = "Start der Veranstaltung ist nach dem Enddatum!"; if (feedback) { Gui.showMessageBox(me, msg, MessageType.ERROR, LOGGER, null); } lblError.setText(msg); return false; } /** * Enables/Disables the tabs based on the given flag 'editable'. * * @param editable when true, will enable the tabs if the user is allowed to see them. * If false, this will disable all tabs but the first tab "About". */ protected void toggleEditable(boolean editable) { // if we don't have a lecture and an image set, just disable editable &= (LecturePerms.canEdit(lecture)); // enable the standard tabs that are always enabled setTabEnabled(pnlTabGeneral, editable); setTabEnabled(pnlTabPermissions, editable && LecturePerms.canAdmin(lecture)); // enable the other tabs that might have been added to the panel (depends on API version) setTabEnabled(pnlTabRestrictions, editable); setTabEnabled(pnlTabLocations, editable); setTabEnabled(pnlTabRunscript, editable); setTabEnabled(pnlTabNetshare, editable); setTabEnabled(pnlTabLdapFilter, editable); setTabEnabled(pnlTabNetrules, editable); btnChangeOwner.setEnabled(LecturePerms.canAdmin(lecture)); btnDownloadImage.setEnabled(ImagePerms.canDownload(image)); } private void setTabEnabled(JPanel tab, boolean editable) { pnlTabs.setEnabledAt(pnlTabs.indexOfComponent(tab), editable); } /** * 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 changeMonitor.isCurrentlyModified(); } @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 (changeMonitor.isCurrentlyModified() && !Gui.showMessageBox(me, "Änderungen werden verworfen, wollen Sie wirklich schließen?", MessageType.QUESTION_YESNO, null, null)) return; synchronized(me) { lecture = null; image = null; } dispose(); } }