2ca7b3d589c37b8b8e1b12b4e906758e981260bf
[divverent/nexuiz.git] / misc / tools / NexuizDemoRecorder / main / src / main / java / com / nexuiz / demorecorder / application / DemoRecorderApplication.java
1 package com.nexuiz.demorecorder.application;\r
2 \r
3 import java.io.File;\r
4 import java.io.FileInputStream;\r
5 import java.io.FileNotFoundException;\r
6 import java.io.FileOutputStream;\r
7 import java.io.IOException;\r
8 import java.io.ObjectInputStream;\r
9 import java.io.ObjectOutputStream;\r
10 import java.net.MalformedURLException;\r
11 import java.net.URL;\r
12 import java.net.URLClassLoader;\r
13 import java.util.ArrayList;\r
14 import java.util.List;\r
15 import java.util.Properties;\r
16 import java.util.ServiceLoader;\r
17 import java.util.concurrent.CopyOnWriteArrayList;\r
18 \r
19 import com.nexuiz.demorecorder.application.jobs.EncoderJob;\r
20 import com.nexuiz.demorecorder.application.jobs.RecordJob;\r
21 import com.nexuiz.demorecorder.application.jobs.RecordsDoneJob;\r
22 import com.nexuiz.demorecorder.application.plugins.EncoderPlugin;\r
23 import com.nexuiz.demorecorder.ui.DemoRecorderUI;\r
24 \r
25 public class DemoRecorderApplication {\r
26         \r
27         public static class Preferences {\r
28                 public static final String OVERWRITE_VIDEO_FILE = "Overwrite final video destination file if it exists";\r
29                 public static final String DISABLE_RENDERING = "Disable rendering while fast-forwarding";\r
30                 public static final String DISABLE_SOUND = "Disable sound while fast-forwarding";\r
31                 public static final String FFW_SPEED_FIRST_STAGE = "Fast-forward speed (first stage)";\r
32                 public static final String FFW_SPEED_SECOND_STAGE = "Fast-forward speed (second stage)";\r
33                 public static final String DO_NOT_DELETE_CUT_DEMOS = "Do not delete cut demos";\r
34                 public static final String JOB_NAME_APPEND_DUPLICATE = "Append this suffix to job-name when duplicating jobs";\r
35                 \r
36                 public static final String[] PREFERENCES_ORDER = {\r
37                         OVERWRITE_VIDEO_FILE,\r
38                         DISABLE_RENDERING,\r
39                         DISABLE_SOUND,\r
40                         FFW_SPEED_FIRST_STAGE,\r
41                         FFW_SPEED_SECOND_STAGE,\r
42                         DO_NOT_DELETE_CUT_DEMOS,\r
43                         JOB_NAME_APPEND_DUPLICATE\r
44                 };\r
45         }\r
46         \r
47         public static final String PREFERENCES_DIRNAME = "settings";\r
48         public static final String LOGS_DIRNAME = "logs";\r
49         public static final String PLUGINS_DIRNAME = "plugins";\r
50         public static final String APP_PREFERENCES_FILENAME = "app_preferences.xml";\r
51         public static final String JOBQUEUE_FILENAME = "jobs.dat";\r
52         \r
53         public static final int STATE_WORKING = 0;\r
54         public static final int STATE_IDLE = 1;\r
55         \r
56         private RecorderJobPoolExecutor poolExecutor;\r
57         private List<RecordJob> jobs;\r
58         private NDRPreferences preferences = null;\r
59         private List<DemoRecorderUI> registeredUserInterfaces;\r
60         private List<EncoderPlugin> encoderPlugins;\r
61         private int state = STATE_IDLE;\r
62         \r
63         public DemoRecorderApplication() {\r
64                 poolExecutor = new RecorderJobPoolExecutor();\r
65                 jobs = new CopyOnWriteArrayList<RecordJob>();\r
66                 this.registeredUserInterfaces = new ArrayList<DemoRecorderUI>();\r
67                 this.encoderPlugins = new ArrayList<EncoderPlugin>();\r
68                 this.getPreferences();\r
69                 this.loadPlugins();\r
70                 this.configurePlugins();\r
71                 this.loadJobQueue();\r
72         }\r
73         \r
74         public void setPreference(String category, String preference, boolean value) {\r
75                 this.preferences.setProperty(category, preference, String.valueOf(value));\r
76         }\r
77         \r
78         public void setPreference(String category, String preference, int value) {\r
79                 this.preferences.setProperty(category, preference, String.valueOf(value));\r
80         }\r
81         \r
82         public void setPreference(String category, String preference, String value) {\r
83                 this.preferences.setProperty(category, preference, value);\r
84         }\r
85         \r
86         public NDRPreferences getPreferences() {\r
87                 if (this.preferences == null) {\r
88                         this.preferences = new NDRPreferences();\r
89                         this.createPreferenceDefaultValues();\r
90                         File preferencesFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, APP_PREFERENCES_FILENAME);\r
91                         if (preferencesFile.exists()) {\r
92                                 FileInputStream fis = null;\r
93                                 try {\r
94                                         fis = new FileInputStream(preferencesFile);\r
95                                         this.preferences.loadFromXML(fis);\r
96                                 } catch (Exception e) {\r
97                                         DemoRecorderUtils.showNonCriticalErrorDialog("Could not load the application preferences file!", e, true);\r
98                                 }\r
99                         }\r
100                 }\r
101                 \r
102                 return this.preferences;\r
103         }\r
104         \r
105         private void createPreferenceDefaultValues() {\r
106                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.OVERWRITE_VIDEO_FILE, "false");\r
107                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_RENDERING, "true");\r
108                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_SOUND, "true");\r
109                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_FIRST_STAGE, "100");\r
110                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_SECOND_STAGE, "10");\r
111                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DO_NOT_DELETE_CUT_DEMOS, "false");\r
112                 this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.JOB_NAME_APPEND_DUPLICATE, " duplicate");\r
113         }\r
114         \r
115         public void savePreferences() {\r
116                 File preferencesFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, APP_PREFERENCES_FILENAME);\r
117                 if (!preferencesFile.exists()) {\r
118                         try {\r
119                                 preferencesFile.createNewFile();\r
120                         } catch (IOException e) {\r
121                                 File parentDir = preferencesFile.getParentFile();\r
122                                 if (!parentDir.exists()) {\r
123                                         try {\r
124                                                 if (parentDir.mkdirs() == true) {\r
125                                                         try {\r
126                                                                 preferencesFile.createNewFile();\r
127                                                         } catch (Exception ex) {}\r
128                                                 }\r
129                                         } catch (Exception ex) {}\r
130                                 }\r
131                         }\r
132                 }\r
133                 \r
134                 if (!preferencesFile.exists()) {\r
135                         DemoRecorderException ex = new DemoRecorderException("Could not create the preferences file " + preferencesFile.getAbsolutePath());\r
136                         DemoRecorderUtils.showNonCriticalErrorDialog(ex);\r
137                         return;\r
138                 }\r
139                 \r
140                 FileOutputStream fos;\r
141                 try {\r
142                         fos = new FileOutputStream(preferencesFile);\r
143                 } catch (FileNotFoundException e) {\r
144                         DemoRecorderUtils.showNonCriticalErrorDialog("Could not create the preferences file " + preferencesFile.getAbsolutePath() + ". Unsufficient rights?", e, true);\r
145                         return;\r
146                 }\r
147                 try {\r
148                         this.preferences.storeToXML(fos, null);\r
149                 } catch (IOException e) {\r
150                         DemoRecorderUtils.showNonCriticalErrorDialog("Could not create the preferences file " + preferencesFile.getAbsolutePath(), e, true);\r
151                 }\r
152         }\r
153         \r
154         public List<RecordJob> getRecordJobs() {\r
155                 return new ArrayList<RecordJob>(this.jobs);\r
156         }\r
157         \r
158         public void startRecording() {\r
159                 if (this.state != STATE_WORKING) {\r
160                         this.state = STATE_WORKING;\r
161                         \r
162                         for (RecordJob currentJob : this.jobs) {\r
163                                 if (currentJob.getState() == RecordJob.State.WAITING) {\r
164                                         this.poolExecutor.runJob(currentJob);\r
165                                 }\r
166                         }\r
167                         \r
168                         //notify ourself when job is done\r
169                         this.poolExecutor.runJob(new RecordsDoneJob(this));\r
170                 }\r
171         }\r
172         \r
173         public void recordSelectedJobs(List<RecordJob> jobList) {\r
174                 if (this.state == STATE_IDLE) {\r
175                         this.state = STATE_WORKING;\r
176                         for (RecordJob currentJob : jobList) {\r
177                                 if (currentJob.getState() == RecordJob.State.WAITING) {\r
178                                         this.poolExecutor.runJob(currentJob);\r
179                                 }\r
180                         }\r
181                         \r
182                         //notify ourself when job is done\r
183                         this.poolExecutor.runJob(new RecordsDoneJob(this));\r
184                 }\r
185         }\r
186         \r
187         public void executePluginForSelectedJobs(EncoderPlugin plugin, List<RecordJob> jobList) {\r
188                 if (this.state == STATE_IDLE) {\r
189                         this.state = STATE_WORKING;\r
190                         for (RecordJob currentJob : jobList) {\r
191                                 if (currentJob.getState() == RecordJob.State.DONE) {\r
192                                         this.poolExecutor.runJob(new EncoderJob(currentJob, plugin));\r
193                                 }\r
194                         }\r
195                         \r
196                         //notify ourself when job is done\r
197                         this.poolExecutor.runJob(new RecordsDoneJob(this));\r
198                 }\r
199         }\r
200         \r
201         public void notifyAllJobsDone() {\r
202                 this.state = STATE_IDLE;\r
203                 \r
204                 //notify all UIs\r
205                 for (DemoRecorderUI currentUI : this.registeredUserInterfaces) {\r
206                         currentUI.recordingFinished();\r
207                 }\r
208         }\r
209         \r
210         public synchronized void stopRecording() {\r
211                 if (this.state == STATE_WORKING) {\r
212                         //clear the queue of the threadpoolexecutor and add the GUI/applayer notify job again\r
213                         this.poolExecutor.clearUnfinishedJobs();\r
214                         this.poolExecutor.runJob(new RecordsDoneJob(this));\r
215                 }\r
216         }\r
217         \r
218         public RecordJob createRecordJob(\r
219                 String name,\r
220                 File enginePath,\r
221                 String engineParameters,\r
222                 File demoFile,\r
223                 String relativeDemoPath,\r
224                 File dpVideoPath,\r
225                 File videoDestination,\r
226                 String executeBeforeCap,\r
227                 String executeAfterCap,\r
228                 float startSecond,\r
229                 float endSecond\r
230         ) {\r
231                 int jobIndex = -1;\r
232                 if (name == null || name.equals("")) {\r
233                         //we don't have a name, so use a generic one \r
234                         jobIndex = this.getNewJobIndex();\r
235                         name = "Job " + jobIndex;\r
236                 } else {\r
237                         //just use the name and keep jobIndex at -1. Jobs with real names don't need an index\r
238                 }\r
239                 \r
240                 \r
241                 \r
242                 RecordJob newJob = new RecordJob(\r
243                         this,\r
244                         name,\r
245                         jobIndex,\r
246                         enginePath,\r
247                         engineParameters,\r
248                         demoFile,\r
249                         relativeDemoPath,\r
250                         dpVideoPath,\r
251                         videoDestination,\r
252                         executeBeforeCap,\r
253                         executeAfterCap,\r
254                         startSecond,\r
255                         endSecond\r
256                 );\r
257                 this.jobs.add(newJob);\r
258                 this.fireUserInterfaceUpdate(newJob);\r
259                 \r
260                 return newJob;\r
261         }\r
262         \r
263         public synchronized boolean deleteRecordJob(RecordJob job) {\r
264                 if (!this.jobs.contains(job)) {\r
265                         return false;\r
266                 }\r
267                 \r
268                 //don't delete jobs that are scheduled for execution\r
269                 if (this.poolExecutor.getJobList().contains(job)) {\r
270                         return false;\r
271                 }\r
272                 \r
273                 this.jobs.remove(job);\r
274                 return true;\r
275         }\r
276         \r
277         public void addUserInterfaceListener(DemoRecorderUI ui) {\r
278                 this.registeredUserInterfaces.add(ui);\r
279         }\r
280         \r
281         /**\r
282          * Makes sure that all registered user interfaces can update their view/display.\r
283          * @param job either a job that's new to the UI, or one the UI already knows but of which details changed\r
284          */\r
285         public void fireUserInterfaceUpdate(RecordJob job) {\r
286                 for (DemoRecorderUI ui : this.registeredUserInterfaces) {\r
287                         ui.RecordJobPropertiesChange(job);\r
288                 }\r
289         }\r
290         \r
291         public int getNewJobIndex() {\r
292                 int jobIndex;\r
293                 if (this.jobs.size() == 0) {\r
294                         jobIndex = 1;\r
295                 } else {\r
296                         int greatestIndex = -1;\r
297                         for (RecordJob j : this.jobs) {\r
298                                 if (j.getJobIndex() > greatestIndex) {\r
299                                         greatestIndex = j.getJobIndex();\r
300                                 }\r
301                         }\r
302                         if (greatestIndex == -1) {\r
303                                 jobIndex = 1;\r
304                         } else {\r
305                                 jobIndex = greatestIndex + 1;\r
306                         }\r
307                 }\r
308                 \r
309                 return jobIndex;\r
310         }\r
311         \r
312         private void loadJobQueue() {\r
313                 File defaultFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, JOBQUEUE_FILENAME);\r
314                 this.loadJobQueue(defaultFile);\r
315         }\r
316         \r
317         @SuppressWarnings("unchecked")\r
318         public void loadJobQueue(File path) {\r
319                 if (!path.exists()) {\r
320                         return;\r
321                 }\r
322                 \r
323                 try {\r
324                         FileInputStream fin = new FileInputStream(path);\r
325                         ObjectInputStream ois = new ObjectInputStream(fin);\r
326                         this.jobs = (List<RecordJob>) ois.readObject();\r
327                         for (RecordJob currentJob : this.jobs) {\r
328                                 currentJob.setAppLayer(this);\r
329                         }\r
330                 } catch (Exception e) {\r
331                         DemoRecorderUtils.showNonCriticalErrorDialog("Could not load the job queue file " + path.getAbsolutePath(), e, true);\r
332                 }\r
333                 \r
334         }\r
335         \r
336         public void saveJobQueue() {\r
337                 File defaultFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, JOBQUEUE_FILENAME);\r
338                 this.saveJobQueue(defaultFile);\r
339         }\r
340         \r
341         public void saveJobQueue(File path) {\r
342                 if (!path.exists()) {\r
343                         try {\r
344                                 path.createNewFile();\r
345                         } catch (IOException e) {\r
346                                 File parentDir = path.getParentFile();\r
347                                 if (!parentDir.exists()) {\r
348                                         try {\r
349                                                 if (parentDir.mkdirs() == true) {\r
350                                                         try {\r
351                                                                 path.createNewFile();\r
352                                                         } catch (Exception ex) {}\r
353                                                 }\r
354                                         } catch (Exception ex) {}\r
355                                 }\r
356                         }\r
357                 }\r
358                 \r
359                 String exceptionMessage = "Could not save the job queue file " + path.getAbsolutePath();\r
360                 \r
361                 if (!path.exists()) {\r
362                         DemoRecorderException ex = new DemoRecorderException(exceptionMessage);\r
363                         DemoRecorderUtils.showNonCriticalErrorDialog(ex);\r
364                         return;\r
365                 }\r
366                 \r
367                 //make sure that for the next start of the program the state is set to waiting again\r
368                 for (RecordJob job : this.jobs) {\r
369                         if (job.getState() == RecordJob.State.PROCESSING) {\r
370                                 job.setState(RecordJob.State.WAITING);\r
371                         }\r
372                         job.setAppLayer(null); //we don't want to serialize the app layer!\r
373                 }\r
374                 \r
375                 try {\r
376                         FileOutputStream fout = new FileOutputStream(path);\r
377                         ObjectOutputStream oos = new ObjectOutputStream(fout);\r
378                         oos.writeObject(this.jobs);\r
379                         oos.close();\r
380                 } catch (Exception e) {\r
381                         DemoRecorderUtils.showNonCriticalErrorDialog(exceptionMessage, e, true);\r
382                 }\r
383                 \r
384                 //we sometimes also save the jobqueue and don't exit the program, so restore the applayer again\r
385                 for (RecordJob job : this.jobs) {\r
386                         job.setAppLayer(this);\r
387                 }\r
388         }\r
389         \r
390         public void shutDown() {\r
391                 this.poolExecutor.shutDown();\r
392                 this.savePreferences();\r
393                 this.saveJobQueue();\r
394         }\r
395         \r
396         public int getState() {\r
397                 return this.state;\r
398         }\r
399         \r
400         private void loadPlugins() {\r
401                 File pluginDir = DemoRecorderUtils.computeLocalFile(PLUGINS_DIRNAME, "");\r
402 \r
403                 if (!pluginDir.exists()) {\r
404                         pluginDir.mkdir();\r
405                 }\r
406 \r
407                 File[] jarFiles = pluginDir.listFiles();\r
408 \r
409                 List<URL> urlList = new ArrayList<URL>();\r
410                 for (File f : jarFiles) {\r
411                         try {\r
412                                 urlList.add(f.toURI().toURL());\r
413                         } catch (MalformedURLException ex) {}\r
414                 }\r
415                 ClassLoader parentLoader = Thread.currentThread().getContextClassLoader();\r
416                 URL[] urls = new URL[urlList.size()];\r
417                 urls = urlList.toArray(urls);\r
418                 URLClassLoader classLoader = new URLClassLoader(urls, parentLoader);\r
419                 \r
420                 ServiceLoader<EncoderPlugin> loader = ServiceLoader.load(EncoderPlugin.class, classLoader);\r
421                 for (EncoderPlugin implementation : loader) {\r
422                         this.encoderPlugins.add(implementation);\r
423                 }\r
424         }\r
425         \r
426         private void configurePlugins() {\r
427                 for (EncoderPlugin plugin : this.encoderPlugins) {\r
428                         plugin.setApplicationLayer(this);\r
429                         Properties pluginPreferences = plugin.getGlobalPreferences();\r
430                         for (Object preference : pluginPreferences.keySet()) {\r
431                                 String preferenceString = (String) preference;\r
432                                 \r
433                                 if (this.preferences.getProperty(plugin.getName(), preferenceString) == null) {\r
434                                         String defaultValue = pluginPreferences.getProperty(preferenceString);\r
435                                         this.preferences.setProperty(plugin.getName(), preferenceString, defaultValue);\r
436                                 }\r
437                         }\r
438                 }\r
439         }\r
440 \r
441         public List<EncoderPlugin> getEncoderPlugins() {\r
442                 return encoderPlugins;\r
443         }\r
444 }\r