Source code: com/flexstor/flexdbserver/services/checkincheckout/CheckOutService.java
1 /*
2 * CheckOutService.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.checkincheckout;
12
13 import java.util.Enumeration;
14 import java.util.Hashtable;
15 import java.util.Iterator;
16 import java.util.Vector;
17
18 import com.flexstor.common.constants.ActionPropertiesI;
19 import com.flexstor.common.constants.ServicesI;
20 import com.flexstor.common.data.ActionData;
21 import com.flexstor.common.data.ActionResult;
22 import com.flexstor.common.data.AssetRecordData;
23 import com.flexstor.common.errorlogger.FlexError;
24 import com.flexstor.common.gateway.CheckOutCheckInGateway;
25 import com.flexstor.common.gateway.exceptions.TransactionFailedException;
26 import com.flexstor.common.resources.Resources;
27 import com.flexstor.common.services.ServiceBrokerI;
28 import com.flexstor.common.services.SrvcNotAvailException;
29 import com.flexstor.common.settings.Settings;
30 import com.flexstor.common.threadmgr.ThreadCallbackI;
31 import com.flexstor.common.threadmgr.ThreadConsumerI;
32 import com.flexstor.flexdbserver.services.Service;
33 import com.flexstor.flexdbserver.services.ServiceContext;
34 import com.flexstor.flexdbserver.transactionmanager.TransMngCheckOut;
35
36 /**
37 * <P>
38 * CheckOutService <BR>
39 * <BLOCKQUOTE>
40 * Checks out and locks a file for modifications. <BR>
41 * The CheckOut Service is performed in two steps, depending on whether the check-out location is in the
42 * server or in the client's local machine. <BR>
43 * - The first step is to change the database status to in-progress and copy the files to the place specified
44 * by the client. <BR>
45 * - The second step is to update the status in the database to either checked-out (upon success)
46 * or checked-in (upon failure); delete the temporary files if the checked-out location is not local to the
47 * server; and finally send an email confirmation if requested by the client. <BR>
48 * In the case where the checked-out location is local to the client's machine, both steps will be carried on
49 * separately; first the client requests to perform the first step of the service; after completion,
50 * the client copies the files from the temporary location to the its machine, and finally it calls the service
51 * once more to perform the second step. <BR>
52 * If the checked-out location is local to the server, the service will complete both steps without the client
53 * intervention. <BR>
54 * It is very important to call the second step when the checked-out location is local to the client's machine;
55 * otherwise the assets' status will remain in-progress permanently. <BR>
56 * </BLOCKQUOTE>
57 * </P>
58 * <P>
59 * The CheckOut Service supports the following cases: <BR>
60 * 1.- Destination is in client machine, preserve resource fork (assume MacBinary will be created) <BR>
61 * <BLOCKQUOTE>
62 * Create MacBinary in temp location specified by client <BR>
63 * Client then copies file to local machine <BR>
64 * </BLOCKQUOTE>
65 * 2.- Destination is in client machine, do not preserve resource fork <BR>
66 * <BLOCKQUOTE>
67 * Copy data fork in temp location specified by client <BR>
68 * Client then copies file to local machine <BR>
69 * </BLOCKQUOTE>
70 * 3.- Destination is in FlexDBServer, prserve resource fork by creating MacBinary <BR>
71 * <BLOCKQUOTE>
72 * Create MacBinary in destination <BR>
73 * </BLOCKQUOTE>
74 * 4.- Destination is in FlexDBServer, preserve resource fork by copying data+rsrc fork <BR>
75 * <BLOCKQUOTE>
76 * Copy data+rsrc fork in destination <BR>
77 * </BLOCKQUOTE>
78 * 5.- Destination is in FlexDBServer, do not preserve resource fork <BR>
79 * <BLOCKQUOTE>
80 * Copy data fork in destination <BR>
81 * </BLOCKQUOTE>
82 * 6.- Destination is in another server, preserve resource fork (assume MacBinary will be created) <BR>
83 * <BLOCKQUOTE>
84 * Create MacBinary in FlexDBServer's temp location <BR>
85 * NFS copy file to server <BR>
86 * </BLOCKQUOTE>
87 * 7.- Destination is in another server, do not preserve resource fork <BR>
88 * <BLOCKQUOTE>
89 * NFS copy data fork to server <BR>
90 * </BLOCKQUOTE>
91 * </P>
92 *
93 * <P>
94 * Input Data Object <BR>
95 * <BLOCKQUOTE>
96 * com.flexstor.common.data.ActionData
97 * </BLOCKQUOTE>
98 * </P>
99 *
100 * <P>
101 * Output Data Object <BR>
102 * <BLOCKQUOTE>
103 * com.flexstor.common.data.ActionResult
104 * </BLOCKQUOTE>
105 * </P>
106 *
107 * Programmable Properties (passed inside data object) <BR>
108 * <BLOCKQUOTE>
109 * Global Properties (apply to all assets) <BR>
110 * <BLOCKQUOTE>
111 * <P>
112 * SourceLocation: Full path where files will be checked out; if files are to be checked out
113 * to a local machine or another server, this property needs to be set to a directory called after
114 * the transaction ID inside the public temp directory. <BR>
115 * Data type: String <BR>
116 * Legal values: Full path to destination location for checked out files <BR>
117 * </P>
118 * <P>
119 * IsLocationInServer: Used by the service to determine if the checked out location is in
120 * another machine or in this server. If checked out location is in this server and
121 * KeepResourceFork is true, the service will copy the file to the specified location,
122 * preserving the resource fork (defaults to true). <BR>
123 * Data type: Boolean <BR>
124 * Legal values: true or false <BR>
125 * </P>
126 * <P>
127 * KeepResourceFork: If set to true, the service will preserve the resource
128 * fork according to the following rules (defaults to true): <BR>
129 * - If IsLocationInServer is true, the file is copied along with its resource fork <BR>
130 * - If IsLocationInServer is set to false, a MacBinary file is created to be moved to the remote machine <BR>
131 * - Otherwise, the resource fork is ignored <BR>
132 * Data type: Boolean <BR>
133 * Legal values: true or false <BR>
134 * </P>
135 * <P>
136 * StepNumber: Defines what step of the check-out service is to be performed. It default to 1. <BR>
137 * Data type: Integer <BR>
138 * Legal values: 1 (check-out, first step) or 2 (check-out, second step) <BR>
139 * </P>
140 * <P>
141 * BadRecordsVector: A Vector containing the list of bad records; this property must be set by the
142 * client before calling the second step of the service. This list is the original list sent by the service
143 * to the client, plus any update made by the client itself in case a file failed to be copied. <BR>
144 * Data type: java.util.Vector <BR>
145 * Legal values: A Vector containing CheckOutRecordData objects <BR>
146 * </P>
147 * <P>
148 * ConfirmData: An optional ActionData object to be used for email confirmation. <BR>
149 * Data type: com.flexstor.common.data.ActionData <BR>
150 * Legal values: A ActionData object with the email address of the sender, subject, cc, and body text
151 * included. The CheckOut Service will attach the path to the successful assets at the end of the body. <BR>
152 * </P>
153 *
154 * <BLOCKQUOTE>
155 * Specific Properties (apply to each individual asset) <BR>
156 * <BLOCKQUOTE>
157 * <P>
158 * UrlString: An optional html/url path to be added at the end of the body of the MimeData. <BR>
159 * Data type: java.lang.String <BR>
160 * Legal values: The path of the checked-out location for the record in url form <BR>
161 * </P>
162 * </BLOCKQUOTE>
163 * </BLOCKQUOTE>
164 */
165 public class CheckOutService
166 implements Service, ThreadCallbackI
167 {
168 // To get the version number from MKS
169 public final static String IDENTIFIER="$Id: CheckOutService.java,v 1.5 2003/08/11 02:22:49 aleric Exp $";
170
171 protected ActionData data = null;
172 protected ActionResult result = null;
173 protected Vector vRecords = null;
174 protected Vector vBadRecords = null;
175 protected int nManagerRunning = 0;
176 protected String sThisService = "";
177 protected boolean bIsLocationInServer = true;
178 protected boolean bFirstStepSuccessful = true;
179
180 protected ServiceContext context;
181 protected int id = -1;
182 protected String fileSeparator;
183 protected ServiceBrokerI serviceBroker;
184
185 /**
186 * Calls before the service is initialized (before initData is called) to
187 * pass information about the environment in which the service is running.
188 * This environment consists of information about the properties set for the
189 * service in one of these files (services.config, roletype_services.config,
190 * or *.ctl), plus methods to access other information such as an instance
191 * of the service broker to invoke other services, the transaction id for
192 * the service, file separator character and local path for the installation
193 * directory and configuration directory.
194 *
195 * @param context Holds information about the environment in which the service
196 * is running.
197 */
198 public void setServiceContext( ServiceContext context )
199 {
200 this.context = context;
201 id = context.getTransactionId();
202 fileSeparator = context.getFileSeparator();
203 serviceBroker = context.getServiceBroker();
204 }
205
206 /**
207 * A data initialization method called at the beginning of the service.
208 * The input argument, ActionData must be cast into its subclass in order
209 * to extract the CheckOutService specific data from it.
210 */
211 public void initData(ActionData actionData)
212 {
213 this.data = actionData;
214 vRecords = data.getRecords();
215 // If vBadRecords is null, initialize to an empty Vector to avoid a NullPointerException
216 vBadRecords = (Vector)data.getObject( ActionPropertiesI.BADRECORDS_VECTOR );
217 if ( vBadRecords == null )
218 vBadRecords = new Vector();
219
220 //6233=Check-Out Service
221 sThisService = Resources.get(6233) + " (" + id + ")";
222 }
223
224 /**
225 * The start of the CheckOut Service.
226 */
227 public ActionResult go()
228 {
229 try { bIsLocationInServer = data.getBoolean( ActionPropertiesI.IS_LOCATION_IN_SERVER ).booleanValue(); }
230 catch ( NullPointerException npe ) {}
231
232 int nStepNumber = 1;
233 try { nStepNumber = data.getInteger( ActionPropertiesI.STEP_NUMBER ).intValue(); }
234 catch ( NullPointerException npe ) {}
235 catch ( NumberFormatException nfe ) {}
236
237 switch ( nStepNumber )
238 {
239 case 1:
240 bFirstStepSuccessful = doCheckOutFirstStep();
241 // Don't perform the second step of check-out (updating status to checked out in db) if the
242 // location is in the client machine (client will ask for the second step manually)
243 if ( !bIsLocationInServer )
244 break;
245 case 2:
246 doCheckOutSecondStep();
247 }
248
249 // Remove this persisted transaction from TransList.per
250 (new TransMngCheckOut( data.getTransId() )).setTransactionCompletedState();
251
252 if ( vBadRecords.size() > 0 )
253 {
254 result = new ActionResult( false );
255 result.setBadRecords( vBadRecords );
256 // Update the list of record with the good ones only, if any
257 data.setRecords( vRecords );
258 }
259 else
260 result = new ActionResult( true );
261
262 result.setId( data.getTransId() );
263 result.setData( data );
264 return result;
265 }
266
267 /**
268 * Performs the following check-out steps:
269 * - Change the database status to in-progress
270 * - Copy the file to the place specified by the client
271 * This service will return false only if the status could not be changed to in-progress.
272 */
273 private boolean doCheckOutFirstStep()
274 {
275 // Set the status to check-out in progress
276 if ( setStatusToInProgress() )
277 {
278 // Create subsets of data object per server and start a new ServiceManager
279 //for each.
280 Hashtable htSubSets = createSubSets();
281
282 if ( htSubSets != null && htSubSets.size() > 0 )
283 {
284 // Create one thread per data object SubSet and start a Service Manager
285 // synchronized to make sure we start all the threads before getting any response back
286 // from observables.
287 synchronized (this)
288 {
289 for (Enumeration e = htSubSets.keys(); e.hasMoreElements(); )
290 {
291 String sServerName = (String) e.nextElement();
292 ActionData dataSubSet = (ActionData)htSubSets.get(sServerName);
293 (new CheckOutServiceManager( sServerName, dataSubSet, this )).startManager();
294 nManagerRunning++;
295 }
296 }
297 }
298 else
299 {
300 // Set all records to bad and remove the Vector of good records
301 vBadRecords = data.getRecords();
302 vRecords = null;
303 }
304 }
305 else
306 {
307 // Set all records to bad and remove the Vector of good records
308 vBadRecords = data.getRecords();
309 vRecords = null;
310 return false;
311 }
312
313 // while there are services still running, just wait until be notified of an update;
314 // if all services are done, exit loop and exit Check-Out Service
315 synchronized (this)
316 {
317 while ( nManagerRunning > 0 )
318 {
319 try
320 {
321 this.wait();
322 }
323 catch (InterruptedException ie)
324 {
325 // Set all records to bad and remove the Vector of good records
326 vBadRecords = data.getRecords();
327 vRecords = null;
328 }
329 }
330 }
331 return true;
332 }
333
334 /**
335 * Performs the following check-out steps:
336 * - Update the status in the database to either checked-out (upon success) or checked-in
337 * (upon failure)
338 * - If the checked out location is not in server, deletes the temporary files created
339 * - If confirmation email is requested, send it
340 */
341 protected void doCheckOutSecondStep()
342 {
343 // If the first step was successful, then we attempt to set the database status to either
344 // checked-out or checked-in. A failure in the first step means that the database status
345 // couldn't be changed to in-progress, in which case, it doesn't make sense to change the
346 // status at this point.
347 if ( bFirstStepSuccessful )
348 setStatusFromInProgress();
349
350 // If the checked-out location is not in the server, we should remove the files from its
351 // temporary location
352 if ( !bIsLocationInServer )
353 deleteTemporaryDir();
354
355 // Finally check if an email confirmation had been requested
356 ActionData confirmData = (ActionData) data.getObject( ActionPropertiesI.CONFIRMATION_DATA );
357 if ( confirmData != null )
358 {
359 if ( vRecords.size() > 0 )
360 {
361 StringBuffer sbBody = new StringBuffer( confirmData.getString( ActionPropertiesI.EMAIL_MESSAGE ) + "\r\n" );
362 AssetRecordData record;
363 String sRecordLocation;
364 for ( int i = 0; i < vRecords.size(); i++ )
365 {
366 record = (AssetRecordData) vRecords.elementAt(i);
367 sRecordLocation = record.getString( ActionPropertiesI.URL_STRING );
368 if ( sRecordLocation == null )
369 sRecordLocation = record.getDestinationFilePath();
370 sbBody.append( " " + sRecordLocation );
371 }
372
373 confirmData.setString( ActionPropertiesI.EMAIL_MESSAGE, sbBody.toString() );
374 }
375 else
376 {
377 // 7047=%%1 failed. See daily log file for details.
378 confirmData.setString( ActionPropertiesI.EMAIL_MESSAGE, Resources.get( 7047, sThisService ) );
379 }
380
381 try{ serviceBroker.invokeImmediately( confirmData ); }
382 catch ( SrvcNotAvailException snae ) {}
383 }
384 }
385
386 /**
387 * Set the status in the database to in-progress
388 */
389 private boolean setStatusToInProgress()
390 {
391 CheckOutCheckInGateway gateway = new CheckOutCheckInGateway();
392 try
393 {
394 gateway.connect();
395 long[] laBadAssets = gateway.updateToInProcessCheckOut( data );
396 if ( laBadAssets != null && laBadAssets.length > 0 )
397 {
398 // if all records failed, return false
399 if ( laBadAssets.length == vRecords.size() )
400 {
401 vBadRecords = vRecords;
402 vRecords = null;
403 return false;
404 }
405 else
406 {
407 // remove the records that could not be updated
408 AssetRecordData record;
409 for ( int i = 0; i < laBadAssets.length; i++ )
410 {
411 for ( Iterator it = vRecords.iterator(); it.hasNext(); )
412 {
413 record = (AssetRecordData) it.next();
414 if ( record.getRecordId() == laBadAssets[i] )
415 {
416 vBadRecords.addElement( record );
417 it.remove();
418 }
419 }
420 }
421 }
422 }
423 return true;
424 }
425 catch ( TransactionFailedException tfe )
426 {
427 // 7046=Unable to change database status for this transaction.
428 new FlexError( FlexError.CRITICAL, sThisService, IDENTIFIER, 7046 );
429 return false;
430 }
431 finally
432 {
433 gateway.dispose();
434 }
435 }
436
437 /**
438 * Change the database status from in-progress to:
439 * - checked-out for successful assets
440 * - checked-in for failed assets
441 */
442 private boolean setStatusFromInProgress()
443 {
444 CheckOutCheckInGateway gateway = new CheckOutCheckInGateway();
445 try
446 {
447 gateway.connect();
448 // Change status to checked-in for bad records
449 if ( vBadRecords.size() > 0 )
450 {
451 ActionData cloneData = (ActionData) data.clone();
452 cloneData.setRecords( vBadRecords );
453 long[] laBadAssets = gateway.rollbackInProcessCheckOut( cloneData );
454 if ( laBadAssets != null && laBadAssets.length > 0 )
455 {
456 // 7045=Failed to rollback status to checked-in for the following assets:
457 StringBuffer sbMessage = new StringBuffer( Resources.get( 7045 ) + "\r\n ");
458 for ( int i = 0; i < laBadAssets.length; i++ )
459 sbMessage.append( laBadAssets[i] + " " );
460 new FlexError( FlexError.CRITICAL, sThisService, IDENTIFIER, sbMessage.toString() );
461 }
462 }
463
464 // Change status to checked-out for good records
465 if ( vRecords.size() > 0 )
466 gateway.checkOutAssets( data );
467
468 return true;
469 }
470 catch ( TransactionFailedException tfe )
471 {
472 // 7046=Unable to change database status for this transaction.
473 new FlexError( FlexError.CRITICAL, sThisService, IDENTIFIER, 7046 );
474 return false;
475 }
476 finally
477 {
478 gateway.dispose();
479 }
480 }
481
482 /**
483 * Deletes the temporary directory used to store the files
484 */
485 private void deleteTemporaryDir()
486 {
487 // Set the temporary directory to {FlexDBServer temp dir}/{trans_id}/
488 String sTempDir = Settings.getString( Settings.APP_SERVER_TMP_DIR );
489 if ( !sTempDir.endsWith( fileSeparator ) )
490 sTempDir += fileSeparator;
491 sTempDir += data.getTransId();
492
493 ActionData deleteData = new ActionData();
494 deleteData.setServiceName( ServicesI.DELETE_SERVICE );
495 deleteData.setString( ActionPropertiesI.LOCATION, sTempDir );
496
497 // Execute Delete Service
498 try { serviceBroker.invokeImmediately( deleteData ); }
499 catch ( SrvcNotAvailException snae ) {}
500 }
501
502 /**
503 * create a Hashtable containing a list of objects to be sent to the service manager.
504 * key = server name; value = ServiceManagerData
505 */
506 private Hashtable createSubSets()
507 {
508 // Retrieve the vector of AssetRecordData and parse thru it; for each server name found
509 // create a ActionData for each new server and add the respective elements to it.
510
511 Hashtable htSubSets = new Hashtable();
512 ActionData dataSubSet;
513 AssetRecordData currRecord;
514 String sServerName;
515
516 if ( vRecords != null)
517 {
518 for ( int i = 0; i < vRecords.size(); i++ )
519 {
520 currRecord = (AssetRecordData)vRecords.elementAt(i);
521 sServerName = currRecord.getServer();
522
523 if ( sServerName != null )
524 {
525 dataSubSet = (ActionData) htSubSets.get( sServerName );
526 if ( dataSubSet == null )
527 {
528 dataSubSet = (ActionData) data.clone();
529 dataSubSet.addRecord( currRecord );
530 htSubSets.put( sServerName, dataSubSet );
531 }
532 else
533 dataSubSet.addRecord( currRecord );
534 }
535 }
536 }
537 return htSubSets;
538 }
539
540 /**
541 * update the ImportData after the set of services are executed for
542 * a ImportData sub set.
543 **/
544 private synchronized void notify( ActionResult result )
545 {
546 Vector vBadRecs = null;
547 if ( result == null )
548 vBadRecs = data.getRecords();
549 else if ( result.isSuccess() == false )
550 vBadRecs = result.getBadRecords();
551
552 if ( vBadRecs != null && vBadRecs.size() > 0 )
553 {
554 AssetRecordData record;
555 for (int i = 0; i < vBadRecs.size(); i++ )
556 {
557 // Set records to bad and remove the Vector of good records
558 record = (AssetRecordData)vBadRecs.elementAt(i);
559 vBadRecords.addElement( record );
560 vRecords.removeElement( record );
561 }
562 }
563
564 nManagerRunning--;
565 this.notifyAll();
566 }
567
568 /**
569 * Called when a thread task is about to begin.
570 * @param consumer the instance that this task will run against.
571 * @param obj the user defined parameter for this task.
572 */
573 public void threadTaskStart ( ThreadConsumerI consumer, Object obj ) {}
574
575 /**
576 * Called when a thread task has just finished.
577 * @param consumer the instance that this task will run against.
578 * @param obj the user defined parameter for this task.
579 */
580 public void threadTaskEnd ( ThreadConsumerI consumer, Object obj )
581 {
582 notify( ((CheckOutServiceManager)consumer).getResult() );
583 }
584 } // CheckOutService