Source code: com/flexstor/flexdbserver/services/external/ExecuteExternalService.java
1 /*
2 * ExecuteExternalService.java
3 *
4 * Copyright $Date: 2003/08/11 02:22:49 $ FLEXSTOR.net Inc.
5 *
6 * This work is licensed for use and distribution under license terms found at
7 * http://www.flexstor.org/license.html
8 *
9 */
10
11 package com.flexstor.flexdbserver.services.external;
12
13 import java.io.IOException;
14 import java.util.ArrayList;
15 import java.util.HashMap;
16 import java.util.Iterator;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Vector;
20
21 import com.flexstor.common.constants.ActionPropertiesI;
22 import com.flexstor.common.constants.AssetRolesI;
23 import com.flexstor.common.constants.ServicesI;
24 import com.flexstor.common.data.ActionData;
25 import com.flexstor.common.data.ActionResult;
26 import com.flexstor.common.data.AssetRecordData;
27 import com.flexstor.common.data.ejb.disguiserecord.DisguiseAssetRecordData;
28 import com.flexstor.common.data.ejb.disguiserecord.DisguiseRecordData;
29 import com.flexstor.common.data.ejb.disguiserecord.GenericBucketRecordData;
30 import com.flexstor.common.errorlogger.FlexError;
31 import com.flexstor.common.importprocessor.ImportData;
32 import com.flexstor.common.resources.Resources;
33 import com.flexstor.common.services.ServiceArgumentsI;
34 import com.flexstor.common.services.SrvcNotAvailException;
35 import com.flexstor.common.util.Diagnostic;
36 import com.flexstor.common.util.DirectDotHot;
37 import com.flexstor.flexdbserver.disguise.BucketHelper;
38 import com.flexstor.flexdbserver.disguise.DisguiseLoaderException;
39 import com.flexstor.flexdbserver.services.Service;
40 import com.flexstor.flexdbserver.services.ServiceContext;
41 import com.flexstor.flexdbserver.services.io.IIntervalTimer;
42 import com.flexstor.flexdbserver.services.io.IntervalTimer;
43
44
45 /**
46 * <P>
47 * ExecuteExternalService <BR>
48 * <BLOCKQUOTE>
49 * The Execute External Service will execute a external program as part of a workflow either
50 * during imports or during a customized process. <BR>
51 * The service can be set so it accepts the full path of the program to be called and its
52 * arguments. Arguments can be meta data information stored in the data object
53 * passed to the service or literal arguments (defined for the service and passed
54 * straight to the program. All arguments should be specified in one of the following files: <BR>
55 * <P>
56 * - services.config <BR>
57 * - roletype_services.config <BR>
58 * - custom_process.xml <BR>
59 * - xml file defined for FLEXsi <BR>
60 * </P>
61 * The following rules apply when defining arguments: <BR>
62 * <P>
63 * - Properties defined in roletype_services.config override properties already defined in
64 * services.config. <BR>
65 * - Properties defined in either custom_process.xml or FLEXsi xml overrides properties
66 * already defined in services.config. <BR>
67 * - The program should be defined to include it full path and name as follows: <BR>
68 * <P>
69 * exceutable = /full/path/to/program/including/name
70 * </P>
71 * - Meta data arguments should be defined as: <BR>
72 * <P>
73 * argN.bucket = <bucket label>, <field label> <BR>
74 * argN+1.bucket = <bucket label>, <field label> <BR>
75 * </P>
76 * - Literal arguments should be defined as: <BR>
77 * <P>
78 * argN+2.literal = <value> <BR>
79 * </P>
80 * where N is the position of this argument as expected in the program. <BR>
81 * </P>
82 * <BR>
83 * When calling this service from a customized process it is a good idea to call the MetaData Service
84 * first, as it will load all bucket, element, asset and asset role information available from the
85 * database. Calling the MetaData Service as part of an import process (before the DBUpdate Service)
86 * will have no effects since information for the assets and any new bucket will not be available yet. <BR>
87 * Due to some limitations on this service at the moment; the ExecuteExternal Service can only be run
88 * against primary assets during an import process. If run as part of a customized process, this limiation
89 * does not apply. <BR>
90 *
91 * </BLOCKQUOTE>
92 * </P>
93 *
94 * <P>
95 * Input Data Object <BR>
96 * <BLOCKQUOTE>
97 * com.flexstor.common.data.ActionData or
98 * com.flexstor.common.importprocessor.ImportData
99 * </BLOCKQUOTE>
100 * </P>
101 *
102 * <P>
103 * Output Data Object <BR>
104 * <BLOCKQUOTE>
105 * com.flexstor.common.data.ActionResult containing a
106 * com.flexstor.common.importprocessor.ActionData object, if called during an import process, or
107 * com.flexstor.common.data.ActionData, if called during a customized process
108 * </BLOCKQUOTE>
109 * </P>
110 *
111 * <P>
112 * Configurable Properties (in services.config or roletype_services.config) and <BR>
113 * Programmable Properties (passed inside data object ) <BR>
114 * <BLOCKQUOTE>
115 * Global Properties (apply to all assets) <BR>
116 * <BLOCKQUOTE>
117 * <P>
118 * inrole: The role of the assets for which to execute this program. Defaults to
119 * ALL if not defined. <BR>
120 * Data type: String <BR>
121 * Legal values: One of the following: HIGHRES, LOWRES, THUMBNAIL, LAYOUT, AUDIO,
122 * VIDEO or ALL <BR>
123 * </P>
124 * <P>
125 * intype: The type of the assets for which to execute this program. Defaults to
126 * ALL if not defined. <BR>
127 * Data type: String <BR>
128 * Legal values: A type as defined in typerdat.txt <BR>
129 * </P>
130 * <P>
131 * inflag: The flag of the assets for which to execute this program. Defaults to
132 * PARENT if not defined. <BR>
133 * Data type: String <BR>
134 * Legal values: One of the following: PARENT, TEMP_PARENT, CHILDREN, TEMP_CHILDREN,
135 * ALL, TEMP_ALL <BR>
136 * </P>
137 * <P>
138 * executable: full path of program to be run by the service. <BR>
139 * Data type: String <BR>
140 * Legal values: /full/path/to/program/including/name <BR>
141 * </P>
142 * <P>
143 * argN.bucket: Bucket and field label for which to use its meta data as argument N
144 * of the program. <BR>
145 * Data type: String <BR>
146 * Legal values: A String of the following form <bucket label>, <field label> <BR>
147 * </P>
148 * <P>
149 * argN.literal: Literal value to be passed directly to the program as argument N. <BR>
150 * Data type: String <BR>
151 * Legal values: A String with an argument for the program <BR>
152 * </P>
153 * <P>
154 * intervaltimer_timeout: A user-specified property in services.config or roletype_services.config
155 * that defines the timeout period for the watch-dog timer that interrupts the external process if
156 * it hangs.
157 * Data type: int <BR>
158 * Legal values: time (seconds) <BR>
159 * </P>
160 * </BLOCKQUOTE>
161 * </BLOCKQUOTE>
162 * </P>
163 */
164 public class ExecuteExternalService
165 implements Service, IIntervalTimer
166 {
167 // To get the version number from MKS
168 public final static String IDENTIFIER="$Id: ExecuteExternalService.java,v 1.3 2003/08/11 02:22:49 aleric Exp $";
169
170 private ActionData data;
171 private String sThisService;
172 private Process pr;
173 private String sCommand;
174 protected String fileSeparator;
175 protected ServiceContext context;
176
177 /**
178 * Holds all arguments passed to this service, which will be passed to the program.
179 * The first item on the list is the program name itself.
180 */
181 private List lExecArgs = new ArrayList();
182
183 /**
184 * Holds the property names; in other words, while the instance variable lExecArgs will
185 * hold only the values, this List will hold the name of the properties.
186 */
187 private List lExecProps = new ArrayList();
188
189 private String sInRole;
190 private String sInType;
191 private String sInFlag;
192
193 private BucketHelper bucketHelper;
194
195 /**
196 * Calls before the service is initialized (before initData is called) to
197 * pass information about the environment in which the service is running.
198 * This environment consists of information about the properties set for the
199 * service in one of these files (services.config, roletype_services.config,
200 * or *.ctl), plus methods to access other information such as an instance
201 * of the service broker to invoke other services, the transaction id for
202 * the service, file separator character and local path for the installation
203 * directory and configuration directory.
204 *
205 * @param context Holds information about the environment in which the service
206 * is running.
207 */
208 public void setServiceContext( ServiceContext context )
209 {
210 this.context = context;
211 fileSeparator = context.getFileSeparator();
212 }
213
214 public void initData( ActionData actionData )
215 {
216 this.data = actionData;
217
218 //5475=Execute External Service
219 sThisService = Resources.get( 5475 );
220 }
221
222 public ActionResult go()
223 {
224 ActionResult result = new ActionResult(false);
225 ImportData importData;
226 // If the ActionData is not an instance of ImportData, convert it to one
227 if ( data instanceof ImportData )
228 {
229 importData = (ImportData) data;
230 result.setData( importData );
231 }
232 else
233 {
234 importData = createImportData( data );
235 result.setData( data );
236 }
237
238 if ( importData != null )
239 {
240 // Set the instance variable lExecArgs with the name and a list of the arguments to be
241 // passed to the program. It first read the properties set for the service (in services.config
242 // or roletype_services.config) and then the ones set in the data object through an xml file
243 // (either custom_process.xml or FLEXsi xml). Any argument in the data object will override an
244 // argument already defined in the service.
245 // These methods also reads the inrole, intype and inflag properties to find the assets in
246 // ImportData for which to execute the program.
247 retrieveServiceArgs();
248 retrieveDataObjectArgs( importData );
249
250 DisguiseRecordData disguiseRef = importData.getDisguiseRecordRef();
251 Vector vAssets = disguiseRef.getAssets( sInRole, sInType, sInFlag );
252 if ( vAssets != null && vAssets.size() > 0 )
253 {
254 try
255 {
256 // load the BucketHelper, most likely we will need it
257 bucketHelper = new BucketHelper( disguiseRef.getDisguiseId() );
258
259 DisguiseAssetRecordData asset;
260 for ( Iterator i = vAssets.iterator(); i.hasNext(); )
261 {
262 asset = (DisguiseAssetRecordData) i.next();
263 try
264 {
265 // Before executing the program, we have to go through the list of arguments and replace any
266 // 'bucket' argument with its real value from the meta data
267 List lThisAssetArgs = getMetaDataArguments( asset );
268 // If the executeProgram goes fine, return successfully!!!!
269 if ( executeProgram(lThisAssetArgs) )
270 result.setSuccess(true);
271 }
272 catch ( Exception e ) { importData.addErrorRecord( new FlexError( FlexError.CRITICAL, sThisService, IDENTIFIER, e.getMessage() ) ); }
273 }
274 }
275 catch ( DisguiseLoaderException dle ) { importData.addErrorRecord( new FlexError( FlexError.CRITICAL, sThisService, IDENTIFIER, dle.getMessage() ) ); }
276 }
277 else
278 //6290=Unable to find assets of the following role, type and flag: %%1 %%2 %%3.
279 importData.addErrorRecord( new FlexError( FlexError.CRITICAL, sThisService, IDENTIFIER, 6290, new String[] { sInRole, sInType, sInFlag } ) );
280 }
281 else
282 // 6291=Cannot operate on an empty data object.
283 new FlexError( FlexError.CRITICAL, sThisService, IDENTIFIER, 6291 );
284
285 return result;
286 }
287
288 private ImportData createImportData( ActionData data )
289 {
290 // Call the ImportDataCreateService in order to convert the ActionData object into
291 // a ImportData object, this way it will be much easier to parse through it.
292 // Before calling the ImportDataCreateService, we first need to set the property that will specify
293 // where the hot directory is. Use the first AssetRecordData in ActionData
294 DirectDotHot refDirectDotHot = new DirectDotHot();
295
296 AssetRecordData record = (AssetRecordData) data.getRecords().elementAt(0);
297 String sRecordCtlFilePath = refDirectDotHot.getCtlFileName( fileSeparator + record.getLocation(), true );
298 data.setString( ActionPropertiesI.HOT_DIRECTORY, sRecordCtlFilePath );
299 data.setServiceName( ServicesI.IMPORTDATA_CREATE_SERVICE );
300 ActionResult srvResult = null;
301 try { srvResult = (ActionResult) context.getServiceBroker().invokeImmediately(data); }
302 catch ( SrvcNotAvailException snae ) {}
303
304 if ( srvResult != null && srvResult.isSuccess() )
305 return (ImportData) srvResult.getData();
306 else
307 return null;
308 }
309
310 /**
311 * Retrieves arguments set in the service instance itself.
312 */
313 private void retrieveServiceArgs()
314 {
315 sInRole = context.getProperty( ServiceArgumentsI.ROLE_DATA_SOURCE );
316 sInType = context.getProperty( ServiceArgumentsI.TYPE_DATA_SOURCE );
317 sInFlag = context.getProperty( ServiceArgumentsI.FLAG_DATA_SOURCE );
318
319 retrieveArgs( context.getProperties().entrySet().iterator() );
320 }
321
322 /**
323 * Retrieves arguments set in the data object
324 */
325 private void retrieveDataObjectArgs( ImportData data )
326 {
327 String sTemp;
328 if ( (sTemp = data.getString(ServiceArgumentsI.ROLE_DATA_SOURCE)) != null )
329 sInRole = sTemp;
330
331 if ( (sTemp = data.getString(ServiceArgumentsI.TYPE_DATA_SOURCE)) != null )
332 sInType = sTemp;
333
334 if ( (sTemp = data.getString(ServiceArgumentsI.FLAG_DATA_SOURCE)) != null )
335 sInFlag = sTemp;
336
337 retrieveArgs( data.getGlobalProperties().entrySet().iterator() );
338 }
339
340 private void retrieveArgs( Iterator props )
341 {
342 // Temporary Map to store items before adding them in lExecProps and lExecArgs
343 HashMap htItems = new HashMap();
344 String sKey;
345 Map.Entry entry;
346 for ( ; props.hasNext(); )
347 {
348 entry = (Map.Entry) props.next();
349 sKey = (String) entry.getKey();
350 if ( sKey.equals("executable") )
351 htItems.put( new Integer(0), new String[] { sKey, (String)entry.getValue() } );
352 else if ( sKey.startsWith("arg") )
353 {
354 // Get whatever is between arg and the next '.' to check if it is a number; if it is
355 // then we should insert the property (either "bucket" or "literal" after the '.' in
356 // lExecProps and the value in lExecArgs
357 int nDotIndex = sKey.indexOf('.');
358 if ( nDotIndex > 3 ) // three is the length of arg
359 {
360 try
361 {
362 int nArgIndex = Integer.parseInt(sKey.substring( 3, nDotIndex ));
363 htItems.put( new Integer(nArgIndex), new String[] { sKey.substring(nDotIndex + 1), (String)entry.getValue() } );
364 }
365 catch ( NumberFormatException nfe ) { continue; } // This is not the argument we are looking for
366 }
367 }
368 }
369
370 // Now populate lExecProps and lExecArgs
371 String[] saArguments;
372 for ( int i = 0; i < htItems.size(); i++ )
373 {
374 saArguments = (String[]) htItems.get( new Integer(i) );
375 // The indexes in the Hashtable represents the index at which the items must be placed
376 // in their respective list; if the item index is bigger than the list, then attach to it,
377 // otherwise, substitute the existing value in the list with this new one.
378 if ( i < lExecProps.size() )
379 {
380 lExecProps.set( i, saArguments[0]);
381 lExecArgs.set( i, saArguments[1] );
382 }
383 else
384 {
385 lExecProps.add( i, saArguments[0]);
386 lExecArgs.add( i, saArguments[1] );
387 }
388 }
389 }
390
391 private List getMetaDataArguments( DisguiseAssetRecordData asset )
392 throws IllegalArgumentException
393 {
394 String sLabels = "";
395 String sProp, sBucketLabel, sFieldLabel;
396 int nArgIndex = 0;
397 try
398 {
399 // Clone lExecArgs so it can be used by all assets in this service.
400 // We will return a list of program arguments specific for this asset
401 List lThisAssetArgs = (ArrayList) ((ArrayList)lExecArgs).clone();
402 for ( Iterator i = lExecProps.iterator(); i.hasNext(); nArgIndex++ )
403 {
404 sProp = (String) i.next();
405 if ( sProp.equals("bucket") )
406 {
407 sLabels = (String) lThisAssetArgs.get(nArgIndex);
408 // Extract the bucket and field labels
409 int nCommaIndex = sLabels.indexOf(',');
410 if ( nCommaIndex != -1 )
411 {
412 sBucketLabel = sLabels.substring( 0, nCommaIndex ).trim();
413 sFieldLabel = sLabels.substring( nCommaIndex + 1 ).trim();
414 int nFieldIndex = bucketHelper.getFieldIndex( sBucketLabel, BucketHelper.BY_LABEL, sFieldLabel, BucketHelper.BY_LABEL );
415
416 if ( nFieldIndex == -1 )
417 // 7095=Illegal argument specified: %%1 is not a valid (bucket label, field label) combination
418 throw new IllegalArgumentException( Resources.get(7095, sLabels) );
419
420 if ( sBucketLabel.equals("Asset Bucket") )
421 lThisAssetArgs.set( nArgIndex, asset.getValues()[nFieldIndex].getValue() );
422 else if ( isAssetRoleBucket( sBucketLabel ) )
423 lThisAssetArgs.set( nArgIndex, asset.getAssetRole().getValues()[nFieldIndex].getValue() );
424 else
425 {
426 int nBucketLevel = bucketHelper.getBucketLevel( sBucketLabel, BucketHelper.BY_LABEL );
427 Vector vBuckets = asset.getTraversalPath();
428 GenericBucketRecordData bucket = (GenericBucketRecordData) vBuckets.elementAt(nBucketLevel);
429 lThisAssetArgs.set( nArgIndex, bucket.getValues()[nFieldIndex].getValue() );
430 }
431 }
432 }
433 }
434 return lThisAssetArgs;
435 }
436 catch ( NullPointerException npe )
437 {
438 // A NullPointerException means that there is not value for the argument specified;
439 // treat this as a IllegalArgumentException indicating that an illegal argument was
440 // specified for the program
441 // 5469=Illegal argument specified: No value found for (%%1) for asset %%2, %%3.
442 // Check that value exists in the database and is available to the service.
443 throw new IllegalArgumentException( Resources.get( 5469, sLabels,
444 asset.getLocation(),
445 asset.getFileName() ) );
446 }
447 }
448
449 private boolean isAssetRoleBucket( String sBucketLabel )
450 {
451 for ( int i = 0; i < AssetRolesI.roles.length; i++ )
452 if ( sBucketLabel.equalsIgnoreCase( AssetRolesI.roles[i] ) )
453 return true;
454
455 return false;
456 }
457
458 private boolean executeProgram( List lThisAssetArgs )
459 throws IOException, InterruptedException
460 {
461 // Convert lThisAssetArgs to a String[] to be passed to the Runtime.exec() method
462 String[] saCommand = (String[]) lThisAssetArgs.toArray( new String[] {} );
463 // Construct a String representation of the command executed
464 StringBuffer sbCommand = new StringBuffer();
465 for ( int i = 0; i < saCommand.length; i++ )
466 {
467 sbCommand.append( saCommand[i] );
468 sbCommand.append( ' ' );
469 }
470 sCommand = sbCommand.toString();
471
472 // Create the watchdog timer in a separate thread
473 // Get the timer interval
474 // Default value is 360 seconds
475 int nInterval;
476 try { nInterval = Integer.parseInt( context.getProperty("intervaltimer_timeout") ); }
477 catch ( NumberFormatException nfe ) { nInterval = 360; }
478
479 IntervalTimer watchdogTimer = new IntervalTimer(this, nInterval);
480 watchdogTimer.start();
481
482 Diagnostic.trace( Diagnostic.APPSERVER_SERVICES, "Executing: " + sCommand );
483 pr = Runtime.getRuntime().exec( saCommand );
484 int nReturnedValue = pr.waitFor();
485
486 // Once the process has completed, stop the timer
487 watchdogTimer.stopTimer();
488
489 return nReturnedValue == 0 ? true : false; // zero is a successful return value
490 }
491
492 /**
493 * Called from IntervalTimer if it times out
494 */
495 public void timedOut()
496 {
497 if (pr != null)
498 {
499 // Terminate the external process since it is taking too long
500 pr.destroy();
501 pr = null;
502
503 // 7093=Could not complete process in the time specified
504 Diagnostic.trace( Diagnostic.APPSERVER_SERVICES, Resources.get(7093) );
505
506 // 7094=External process timed out on: %%1
507 new FlexError(FlexError.CRITICAL, IDENTIFIER, sThisService, 7094, new String[] { sCommand } );
508 }
509 }
510 }