View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.workflow.handler.notification;
23  
24  import org.opencastproject.job.api.JobContext;
25  import org.opencastproject.mediapackage.MediaPackage;
26  import org.opencastproject.mediapackage.MediaPackageParser;
27  import org.opencastproject.search.api.SearchService;
28  import org.opencastproject.security.api.UnauthorizedException;
29  import org.opencastproject.serviceregistry.api.ServiceRegistry;
30  import org.opencastproject.util.NotFoundException;
31  import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
32  import org.opencastproject.workflow.api.WorkflowInstance;
33  import org.opencastproject.workflow.api.WorkflowOperationException;
34  import org.opencastproject.workflow.api.WorkflowOperationHandler;
35  import org.opencastproject.workflow.api.WorkflowOperationInstance;
36  import org.opencastproject.workflow.api.WorkflowOperationResult;
37  import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
38  
39  import org.apache.http.HttpResponse;
40  import org.apache.http.NameValuePair;
41  import org.apache.http.auth.AuthScope;
42  import org.apache.http.auth.UsernamePasswordCredentials;
43  import org.apache.http.client.CredentialsProvider;
44  import org.apache.http.client.entity.UrlEncodedFormEntity;
45  import org.apache.http.client.methods.HttpPost;
46  import org.apache.http.impl.client.BasicCredentialsProvider;
47  import org.apache.http.impl.client.CloseableHttpClient;
48  import org.apache.http.impl.client.HttpClientBuilder;
49  import org.apache.http.message.BasicNameValuePair;
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.net.URI;
56  import java.net.URL;
57  import java.util.ArrayList;
58  import java.util.List;
59  import java.util.Set;
60  
61  /**
62   * Workflow Operation for POSTing a MediaPackage via HTTP
63   */
64  @Component(
65      immediate = true,
66      service = WorkflowOperationHandler.class,
67      property = {
68          "service.description=Workflow Operation that POSTs MediaPackages via HTTP",
69          "workflow.operation=post-mediapackage"
70      }
71  )
72  public class MediaPackagePostOperationHandler extends AbstractWorkflowOperationHandler {
73  
74    /** The logging facility */
75    private static final Logger logger = LoggerFactory.getLogger(MediaPackagePostOperationHandler.class);
76  
77    /** search service **/
78    private SearchService searchService;
79  
80    @Reference
81    public void setSearchService(SearchService searchService) {
82      this.searchService = searchService;
83    }
84  
85    @Reference
86    @Override
87    public void setServiceRegistry(ServiceRegistry serviceRegistry) {
88      super.setServiceRegistry(serviceRegistry);
89    }
90  
91    public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
92            throws WorkflowOperationException {
93  
94      // get configuration
95      WorkflowOperationInstance currentOperation = workflowInstance.getCurrentOperation();
96      Configuration config = new Configuration(currentOperation);
97  
98      MediaPackage workflowMP = workflowInstance.getMediaPackage();
99      MediaPackage mp = workflowMP;
100 
101     // check if we need to replace the media package we got with the published
102     // media package from the search service
103     if (config.mpFromSearch()) {
104       logger.info("Getting media package from search service");
105       try {
106         mp = searchService.get(mp.getIdentifier().toString());
107       } catch (NotFoundException | UnauthorizedException e) {
108         throw new WorkflowOperationException("could not get media package " + mp + " from search service.");
109       }
110     }
111 
112     logger.info("Submitting {} ({}) as {} to {}",
113         mp.getTitle(), mp.getIdentifier(), config.getFormat().name(), config.getUrl());
114 
115     try {
116       // serialize MediaPackage to target format
117       String mpStr;
118       if (config.getFormat() == Configuration.Format.JSON) {
119         mpStr = MediaPackageParser.getAsJSON(mp);
120       } else {
121         mpStr = MediaPackageParser.getAsXml(mp);
122       }
123 
124       // Log media packge
125       if (config.debug()) {
126         logger.info(mpStr);
127       }
128 
129       // construct message body
130       List<NameValuePair> data = new ArrayList<>();
131       data.add(new BasicNameValuePair("mediapackage", mpStr));
132       data.addAll(config.getAdditionalFields());
133 
134       // construct POST
135       HttpPost post = new HttpPost(config.getUrl());
136       post.setEntity(new UrlEncodedFormEntity(data, config.getEncoding()));
137 
138       // execute POST
139       HttpClientBuilder clientBuilder = HttpClientBuilder.create();
140 
141       // Handle authentication
142       if (config.authenticate()) {
143         URL targetUrl = config.getUrl().toURL();
144         CredentialsProvider provider = new BasicCredentialsProvider();
145         provider.setCredentials(
146             new AuthScope(targetUrl.getHost(), targetUrl.getPort()),
147             config.getCredentials());
148         clientBuilder.setDefaultCredentialsProvider(provider);
149       }
150       CloseableHttpClient client = clientBuilder.build();
151 
152       HttpResponse response = client.execute(post);
153 
154       // throw Exception if target host did not return 200
155       int status = response.getStatusLine().getStatusCode();
156       if ((status >= 200) && (status < 300)) {
157         if (config.debug()) {
158           logger.info("Successfully submitted '{}' ({}) to {}: {}", mp.getTitle(), mp.getIdentifier(),
159               config.getUrl(), status);
160         }
161       } else if (status == 418) {
162         logger.warn("Submitted '{}' ({}) to {}: The target claims to be a teapot. "
163                 + "The Reason for this is probably an insane developer. Go and help that person!",
164             mp.getTitle(), mp.getIdentifier(), config.getUrl());
165       } else {
166         throw new WorkflowOperationException("Failed to submit \"" + mp.getTitle()
167             + "\" (" + mp.getIdentifier().toString() + "), " + config.getUrl().toString()
168             + " answered with: " + Integer.toString(status));
169       }
170     } catch (Exception e) {
171       if (e instanceof WorkflowOperationException) {
172         throw (WorkflowOperationException) e;
173       } else {
174         throw new WorkflowOperationException(e);
175       }
176     }
177     return createResult(workflowMP, Action.CONTINUE);
178   }
179 
180   // <editor-fold defaultstate="collapsed" desc="Inner class that wraps around this WorkflowOperations Configuration">
181   private static class Configuration {
182 
183     public enum Format {
184       XML, JSON
185     }
186 
187     // Key for the WorkflowOperation Configuration
188     public static final String PROPERTY_URL = "url";
189     public static final String PROPERTY_FORMAT = "format";
190     public static final String PROPERTY_ENCODING = "encoding";
191     public static final String PROPERTY_AUTH = "auth.enabled";
192     public static final String PROPERTY_AUTHUSER = "auth.username";
193     public static final String PROPERTY_AUTHPASSWD = "auth.password";
194     public static final String PROPERTY_DEBUG = "debug";
195     public static final String PROPERTY_MEDIAPACKAGE_TYPE = "mediapackage.type";
196 
197     // Configuration values
198     private URI url;
199     private Format format = Format.XML;
200     private String encoding = "UTF-8";
201     private boolean authenticate = false;
202     private UsernamePasswordCredentials credentials = null;
203     private List<NameValuePair> additionalFields = new ArrayList<NameValuePair>();
204     private boolean debug = false;
205     private boolean mpFromSearch = true;
206 
207     Configuration(WorkflowOperationInstance operation) throws WorkflowOperationException {
208       try {
209         Set<String> keys = operation.getConfigurationKeys();
210 
211         // configure URL
212         if (keys.contains(PROPERTY_URL)) {
213           url = new URI(operation.getConfiguration(PROPERTY_URL));
214         } else {
215           throw new IllegalArgumentException("No target URL provided.");
216         }
217 
218         // configure format
219         if (keys.contains(PROPERTY_FORMAT)) {
220           format = Format.valueOf(operation.getConfiguration(PROPERTY_FORMAT).toUpperCase());
221         }
222 
223         // configure message encoding
224         if (keys.contains(PROPERTY_ENCODING)) {
225           encoding = operation.getConfiguration(PROPERTY_ENCODING);
226         }
227 
228         // configure authentication
229         if (keys.contains(PROPERTY_AUTH)) {
230           String auth = operation.getConfiguration(PROPERTY_AUTH).toUpperCase();
231           if (!("NO").equals(auth) && !("FALSE").equals(auth)) {
232             String username = operation.getConfiguration(PROPERTY_AUTHUSER);
233             String password = operation.getConfiguration(PROPERTY_AUTHPASSWD);
234             if (username == null || password == null) {
235               throw new WorkflowOperationException("Username and Password must be provided for authentication!");
236             }
237             credentials = new UsernamePasswordCredentials(username, password);
238             authenticate = true;
239           }
240         }
241 
242         // Configure debug mode
243         if (keys.contains(PROPERTY_DEBUG)) {
244           String debugstr = operation.getConfiguration(PROPERTY_DEBUG).trim().toUpperCase();
245           debug = "YES".equals(debugstr) || "TRUE".equals(debugstr);
246         }
247 
248         // Configure debug mode
249         if (keys.contains(PROPERTY_MEDIAPACKAGE_TYPE)) {
250           String cfgval = operation.getConfiguration(PROPERTY_MEDIAPACKAGE_TYPE).trim().toUpperCase();
251           mpFromSearch = "SEARCH".equals(cfgval);
252         }
253 
254         // get additional form fields
255         for (String key : operation.getConfigurationKeys()) {
256           if (key.startsWith("+")) {
257             String value = operation.getConfiguration(key);
258             additionalFields.add(new BasicNameValuePair(key.substring(1), value));
259           }
260         }
261       } catch (Exception e) {
262         throw new WorkflowOperationException("Faild to configure operation instance.", e);
263       }
264     }
265 
266     public URI getUrl() {
267       return url;
268     }
269 
270     public Format getFormat() {
271       return format;
272     }
273 
274     public String getEncoding() {
275       return encoding;
276     }
277 
278     public boolean authenticate() {
279       return authenticate;
280     }
281 
282     public UsernamePasswordCredentials getCredentials() {
283       return credentials;
284     }
285 
286     public List<NameValuePair> getAdditionalFields() {
287       return additionalFields;
288     }
289 
290     public boolean debug() {
291       return debug;
292     }
293 
294     public boolean mpFromSearch() {
295       return mpFromSearch;
296     }
297   }
298 
299   // </editor-fold>
300 }