1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.animate.impl;
23
24 import org.opencastproject.animate.api.AnimateService;
25 import org.opencastproject.animate.api.AnimateServiceException;
26 import org.opencastproject.job.api.AbstractJobProducer;
27 import org.opencastproject.job.api.Job;
28 import org.opencastproject.security.api.OrganizationDirectoryService;
29 import org.opencastproject.security.api.SecurityService;
30 import org.opencastproject.security.api.UserDirectoryService;
31 import org.opencastproject.serviceregistry.api.ServiceRegistry;
32 import org.opencastproject.serviceregistry.api.ServiceRegistryException;
33 import org.opencastproject.util.ConfigurationException;
34 import org.opencastproject.util.IoSupport;
35 import org.opencastproject.util.LoadUtil;
36 import org.opencastproject.util.NotFoundException;
37 import org.opencastproject.workspace.api.Workspace;
38
39 import com.google.gson.Gson;
40 import com.google.gson.reflect.TypeToken;
41
42 import org.apache.commons.io.FileUtils;
43 import org.apache.commons.io.FilenameUtils;
44 import org.apache.commons.io.IOUtils;
45 import org.apache.commons.lang3.StringUtils;
46 import org.apache.commons.text.StringEscapeUtils;
47 import org.osgi.service.cm.ManagedService;
48 import org.osgi.service.component.ComponentContext;
49 import org.osgi.service.component.annotations.Activate;
50 import org.osgi.service.component.annotations.Component;
51 import org.osgi.service.component.annotations.Reference;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import java.io.BufferedReader;
56 import java.io.File;
57 import java.io.FileInputStream;
58 import java.io.IOException;
59 import java.io.InputStream;
60 import java.io.InputStreamReader;
61 import java.lang.reflect.Type;
62 import java.net.URI;
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.Dictionary;
66 import java.util.List;
67 import java.util.Map;
68
69
70 @Component(
71 immediate = true,
72 service = {
73 AnimateService.class,
74 ManagedService.class
75 },
76 property = {
77 "service.description=Animation Service",
78 "service.pid=org.opencastproject.animate.impl.AnimateServiceImpl"
79 }
80 )
81 public class AnimateServiceImpl extends AbstractJobProducer implements AnimateService, ManagedService {
82
83
84 private static final String SYNFIG_BINARY_CONFIG = "synfig.path";
85
86
87 public static final String SYNFIG_BINARY_DEFAULT = "synfig";
88
89
90 private String synfigBinary = SYNFIG_BINARY_DEFAULT;
91
92
93 private static final String JOB_LOAD_CONFIG = "job.load.animate";
94
95
96 private static final float JOB_LOAD_DEFAULT = 0.8f;
97
98
99 private float jobLoad = JOB_LOAD_DEFAULT;
100
101 private static final Logger logger = LoggerFactory.getLogger(AnimateServiceImpl.class);
102
103
104 private static final String OPERATION = "animate";
105
106 private Workspace workspace;
107 private ServiceRegistry serviceRegistry;
108 private SecurityService securityService;
109 private UserDirectoryService userDirectoryService;
110 private OrganizationDirectoryService organizationDirectoryService;
111
112 private static final Type stringMapType = new TypeToken<Map<String, String>>() { }.getType();
113 private static final Type stringListType = new TypeToken<List<String>>() { }.getType();
114
115
116 public AnimateServiceImpl() {
117 super(JOB_TYPE);
118 }
119
120 @Override
121 @Activate
122 public void activate(ComponentContext cc) {
123 super.activate(cc);
124 logger.debug("Activated animate service");
125 }
126
127 @Override
128 public void updated(Dictionary properties) throws ConfigurationException {
129 if (properties == null) {
130 return;
131 }
132 logger.debug("Start updating animate service");
133
134 synfigBinary = StringUtils.defaultIfBlank((String) properties.get(SYNFIG_BINARY_CONFIG), SYNFIG_BINARY_DEFAULT);
135 logger.debug("Set synfig binary path to {}", synfigBinary);
136
137 jobLoad = LoadUtil.getConfiguredLoadValue(properties, JOB_LOAD_CONFIG, JOB_LOAD_DEFAULT, serviceRegistry);
138 logger.debug("Set animate job load to {}", jobLoad);
139
140 logger.debug("Finished updating animate service");
141 }
142
143
144
145
146
147
148 @Override
149 protected String process(Job job) throws Exception {
150 logger.debug("Started processing job {}", job.getId());
151 if (!OPERATION.equals(job.getOperation())) {
152 throw new ServiceRegistryException(String.format("This service can't handle operations of type '%s'",
153 job.getOperation()));
154 }
155
156 List<String> arguments = job.getArguments();
157 URI animation = new URI(arguments.get(0));
158 Gson gson = new Gson();
159 Map<String, String> metadata = gson.fromJson(arguments.get(1), stringMapType);
160 List<String> options = gson.fromJson(arguments.get(2), stringListType);
161
162
163 File input = customAnimation(job, animation, metadata);
164
165
166 File output = new File(workspace.rootDirectory(), String.format("animate/%d/%s.%s", job.getId(),
167 FilenameUtils.getBaseName(animation.getPath()), "mkv"));
168 FileUtils.forceMkdirParent(output);
169
170
171 final List<String> command = new ArrayList<>();
172 command.add(synfigBinary);
173 command.add("-i");
174 command.add(input.getAbsolutePath());
175 command.add("-o");
176 command.add(output.getAbsolutePath());
177 command.addAll(options);
178 logger.info("Executing animation command: {}", command);
179
180 Process process = null;
181 try {
182 ProcessBuilder processBuilder = new ProcessBuilder(command);
183 processBuilder.redirectErrorStream(true);
184 process = processBuilder.start();
185
186
187 try (BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
188 String line;
189 while ((line = in.readLine()) != null) {
190 logger.debug("Synfig: {}", line);
191 }
192 }
193
194
195 int exitCode = process.waitFor();
196 if (exitCode != 0) {
197 throw new AnimateServiceException(String.format("Synfig exited abnormally with status %d (command: %s)",
198 exitCode, command));
199 }
200 if (!output.isFile()) {
201 throw new AnimateServiceException("Synfig produced no output");
202 }
203 logger.info("Animation generated successfully: {}", output);
204 } catch (Exception e) {
205
206 FileUtils.deleteQuietly(output.getParentFile());
207 logger.debug("Removed output directory of failed animation process: {}", output.getParentFile());
208 throw new AnimateServiceException(e);
209 } finally {
210 IoSupport.closeQuietly(process);
211 FileUtils.deleteQuietly(input);
212 }
213
214 URI uri = workspace.putInCollection("animate-" + job.getId(), output.getName(),
215 new FileInputStream(output));
216 FileUtils.deleteQuietly(new File(workspace.rootDirectory(), String.format("animate/%d", job.getId())));
217
218 return uri.toString();
219 }
220
221
222 private File customAnimation(final Job job, final URI input, final Map<String, String> metadata)
223 throws IOException, NotFoundException {
224 logger.debug("Start customizing the animation");
225 File output = new File(workspace.rootDirectory(), String.format("animate/%d/%s.%s", job.getId(),
226 FilenameUtils.getBaseName(input.getPath()), FilenameUtils.getExtension(input.getPath())));
227 FileUtils.forceMkdirParent(output);
228 String animation;
229 try {
230 animation = FileUtils.readFileToString(new File(input), "UTF-8");
231 } catch (IOException e) {
232
233 logger.debug("Falling back to workspace to read {}", input);
234 try (InputStream in = workspace.read(input)) {
235 animation = IOUtils.toString(in, "UTF-8");
236 }
237 }
238
239
240 for (Map.Entry<String, String> entry: metadata.entrySet()) {
241 String value = StringEscapeUtils.escapeXml11(entry.getValue());
242 animation = animation.replaceAll("\\{\\{" + entry.getKey() + "\\}\\}", value);
243 }
244
245
246 FileUtils.write(output, animation, "utf-8");
247
248 return output;
249 }
250
251
252 @Override
253 public Job animate(URI animation, Map<String, String> metadata, List<String> arguments)
254 throws AnimateServiceException {
255 Gson gson = new Gson();
256 List<String> jobArguments = Arrays.asList(animation.toString(), gson.toJson(metadata), gson.toJson(arguments));
257 try {
258 logger.debug("Create animate service job");
259 return serviceRegistry.createJob(JOB_TYPE, OPERATION, jobArguments, jobLoad);
260 } catch (ServiceRegistryException e) {
261 throw new AnimateServiceException(e);
262 }
263 }
264
265 @Override
266 protected ServiceRegistry getServiceRegistry() {
267 return serviceRegistry;
268 }
269
270 @Override
271 protected SecurityService getSecurityService() {
272 return securityService;
273 }
274
275 @Override
276 protected UserDirectoryService getUserDirectoryService() {
277 return userDirectoryService;
278 }
279
280 @Override
281 protected OrganizationDirectoryService getOrganizationDirectoryService() {
282 return organizationDirectoryService;
283 }
284
285 @Reference
286 public void setWorkspace(Workspace workspace) {
287 this.workspace = workspace;
288 }
289
290 @Reference
291 public void setServiceRegistry(ServiceRegistry jobManager) {
292 this.serviceRegistry = jobManager;
293 }
294
295 @Reference
296 public void setSecurityService(SecurityService securityService) {
297 this.securityService = securityService;
298 }
299
300 @Reference
301 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
302 this.userDirectoryService = userDirectoryService;
303 }
304
305 @Reference
306 public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
307 this.organizationDirectoryService = organizationDirectoryService;
308 }
309 }