/* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * For information about the authors of this project Have a look * at the AUTHORS file in the root of this project. */ package net.sourceforge.fullsync; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.MalformedURLException; import java.nio.charset.StandardCharsets; import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.Collections; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import net.sourceforge.fullsync.remote.RemoteManager; import net.sourceforge.fullsync.schedule.Schedule; import net.sourceforge.fullsync.schedule.ScheduleTask; import net.sourceforge.fullsync.schedule.ScheduleTaskSource; import net.sourceforge.fullsync.schedule.Scheduler; import net.sourceforge.fullsync.schedule.SchedulerChangeListener; import net.sourceforge.fullsync.schedule.SchedulerImpl; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; // TODO remove schedulerChangeListener /** * A ProfileManager handles persistence of Profiles and provides * a scheduler for creating events when a Profile should be executed. */ public class ProfileManager implements ProfileChangeListener, ScheduleTaskSource, SchedulerChangeListener { class ProfileManagerSchedulerTask implements ScheduleTask { private Profile profile; private long executionTime; ProfileManagerSchedulerTask(Profile profile, long executionTime) { this.profile = profile; this.executionTime = executionTime; } @Override public void run() { Thread worker = new Thread(() -> fireProfileSchedulerEvent(profile)); worker.start(); profile.getSchedule().setLastOccurrence(System.currentTimeMillis()); Thread.yield(); } @Override public long getExecutionTime() { return executionTime; } @Override public String toString() { return "Scheduled execution of " + profile.getName(); } } private String configFile; private ArrayList<Profile> profiles; private ArrayList<ProfileListChangeListener> changeListeners; private ArrayList<ProfileSchedulerListener> scheduleListeners; private boolean remoteConnected = false; // FIXME this list is only needed because we need to give feedback from // the local scheduler and a remote scheduler. private ArrayList<SchedulerChangeListener> schedulerChangeListeners; // TODO the scheduler shouldn't reside within the profile manager // but just use it as task source private Scheduler scheduler; // FIXME omg, a profilemanager having a remoteprofilemanager? // please make a dao of me, with save/load and that's it // don't forget calling profilesChangeEvent if dao is changed private RemoteManager remoteManager; private ProfileListChangeListener remoteListener; protected ProfileManager() { this.profiles = new ArrayList<Profile>(); this.changeListeners = new ArrayList<ProfileListChangeListener>(); this.scheduleListeners = new ArrayList<ProfileSchedulerListener>(); this.schedulerChangeListeners = new ArrayList<SchedulerChangeListener>(); this.scheduler = new SchedulerImpl(this); this.scheduler.addSchedulerChangeListener(this); } public ProfileManager(String configFile) throws SAXException, IOException, ParserConfigurationException, FactoryConfigurationError { this(); this.configFile = configFile; loadProfiles(configFile); Collections.sort(profiles); } public void setRemoteConnection(RemoteManager remoteManager) throws MalformedURLException, RemoteException, NotBoundException { this.remoteManager = remoteManager; remoteListener = new ProfileListChangeListener() { @Override public void profileListChanged() { updateRemoteProfiles(); } @Override public void profileChanged(Profile p) { updateRemoteProfiles(); } }; remoteManager.addProfileListChangeListener(remoteListener); remoteManager.addSchedulerChangeListener(this); updateRemoteProfiles(); fireSchedulerChangedEvent(); } private void updateRemoteProfiles() { this.profiles = new ArrayList<Profile>(); Profile[] remoteprofiles = remoteManager.getProfiles(); for (Profile remoteprofile : remoteprofiles) { this.profiles.add(remoteprofile); remoteprofile.addProfileChangeListener(this); } fireProfilesChangeEvent(); } public void disconnectRemote() { if (remoteManager != null) { try { remoteManager.removeProfileListChangeListener(remoteListener); remoteManager.removeSchedulerChangeListener(this); } catch (RemoteException e) { ExceptionHandler.reportException(e); } remoteManager = null; this.profiles = new ArrayList<Profile>(); try { loadProfiles(configFile); } catch (Exception e) { ExceptionHandler.reportException(e); } remoteConnected = false; fireProfilesChangeEvent(); } } /** * Check for the existence of a successful remote connection. * @return true if a remote connection is active. */ public final boolean isConnected() { return null != remoteManager; } /** * This method in necessary to avoid self RMI connections. * @return true if this instance is connected to another instance, or an attempt to connect is running */ public final boolean isConnectedToRemoteInstance() { return remoteConnected; } public boolean loadProfiles(String profilesFileName) throws SAXException, IOException, ParserConfigurationException, FactoryConfigurationError { File file = new File(profilesFileName); if (file.exists() && (file.length() > 0)) { DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = builder.parse(file); NodeList list = doc.getDocumentElement().getChildNodes(); for (int i = 0; i < list.getLength(); i++) { Node n = list.item(i); if (n.getNodeType() == Node.ELEMENT_NODE) { try { Profile p = Profile.unserialize((Element) n); String name = p.getName(); int j = 1; while (null != getProfile(name)) { name = p.getName() + " (" + (j++) + ")"; } p.setName(name); doAddProfile(p, false); } catch (Throwable t) { ExceptionHandler.reportException("Failed to load a Profile, ignoring and continuing with the rest", t); } } } fireProfilesChangeEvent(); return true; } return false; } private void doAddProfile(Profile profile, boolean fireChangedEvent) { profiles.add(profile); profile.addProfileChangeListener(this); if (fireChangedEvent) { fireProfilesChangeEvent(); } } public void addProfile(Profile profile) { doAddProfile(profile, true); } public void removeProfile(Profile profile) { profile.removeProfileChangeListener(this); profiles.remove(profile); fireProfilesChangeEvent(); } public ArrayList<Profile> getProfiles() { return profiles; } public Profile getProfile(String name) { for (Profile p : profiles) { if (p.getName().equals(name)) { return p; } } return null; } public void startScheduler() { if (remoteManager != null) { remoteManager.startTimer(); } else { scheduler.start(); } } public void stopScheduler() { if (remoteManager != null) { remoteManager.stopTimer(); } else { scheduler.stop(); } } public boolean isSchedulerEnabled() { if (remoteManager != null) { return remoteManager.isSchedulerEnabled(); } return scheduler.isEnabled(); } @Override public ScheduleTask getNextScheduleTask() { long now = System.currentTimeMillis(); long nextTime = Long.MAX_VALUE; Profile nextProfile = null; for (Profile p : profiles) { Schedule s = p.getSchedule(); if (p.isEnabled() && (s != null)) { long o = s.getNextOccurrence(now); if (nextTime > o) { nextTime = o; nextProfile = p; } } } if (nextProfile != null) { return new ProfileManagerSchedulerTask(nextProfile, nextTime); } return null; } public void addProfilesChangeListener(ProfileListChangeListener listener) { changeListeners.add(listener); } public void removeProfilesChangeListener(ProfileListChangeListener listener) { changeListeners.remove(listener); } protected void fireProfilesChangeEvent() { for (ProfileListChangeListener changeListener : changeListeners) { changeListener.profileListChanged(); } } @Override public void profileChanged(Profile profile) { scheduler.refresh(); for (ProfileListChangeListener changeListener : changeListeners) { changeListener.profileChanged(profile); } } public void addSchedulerListener(ProfileSchedulerListener listener) { scheduleListeners.add(listener); } public void removeSchedulerListener(ProfileSchedulerListener listener) { scheduleListeners.remove(listener); } protected void fireProfileSchedulerEvent(Profile profile) { for (ProfileSchedulerListener schedulerListener : scheduleListeners) { schedulerListener.profileExecutionScheduled(profile); } } @Override public void schedulerStatusChanged(boolean status) { fireSchedulerChangedEvent(); } public void addSchedulerChangeListener(SchedulerChangeListener listener) { schedulerChangeListeners.add(listener); } public void removeSchedulerChangeListener(SchedulerChangeListener listener) { schedulerChangeListeners.remove(listener); } protected void fireSchedulerChangedEvent() { boolean enabled = isSchedulerEnabled(); for (SchedulerChangeListener listener : schedulerChangeListeners) { listener.schedulerStatusChanged(enabled); } } public void save() { try { if (remoteManager != null) { remoteManager.removeProfileListChangeListener(remoteListener); remoteManager.save(profiles.toArray(new Profile[0])); remoteManager.addProfileListChangeListener(remoteListener); } else { DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = docBuilder.newDocument(); Element e = doc.createElement("Profiles"); e.setAttribute("version", "1.1"); for (Profile p : profiles) { e.appendChild(p.serialize(doc)); } doc.appendChild(e); TransformerFactory fac = TransformerFactory.newInstance(); fac.setAttribute("indent-number", 2); Transformer tf = fac.newTransformer(); tf.setOutputProperty(OutputKeys.METHOD, "xml"); tf.setOutputProperty(OutputKeys.VERSION, "1.0"); tf.setOutputProperty(OutputKeys.INDENT, "yes"); tf.setOutputProperty(OutputKeys.STANDALONE, "no"); DOMSource source = new DOMSource(doc); try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(configFile + ".tmp"), StandardCharsets.UTF_8)) { tf.transform(source, new StreamResult(osw)); osw.flush(); osw.close(); File newCfgFile = new File(configFile + ".tmp"); if (0 == newCfgFile.length()) { throw new Exception("Storing profiles failed (size = 0)"); } Util.fileRenameToPortableLegacy(configFile + ".tmp", configFile); } catch (Exception ex) { new File(configFile + ".tmp").delete(); throw ex; } } } catch (Exception ex) { ExceptionHandler.reportException(ex); } } public void setRemoteConnected(boolean connected) { remoteConnected = connected; } }