001/*- 002 * #%L 003 * HAPI FHIR JPA Server - Batch2 Task Processor 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.batch2.model; 021 022import ca.uhn.fhir.batch2.api.IJobCompletionHandler; 023import ca.uhn.fhir.batch2.api.IJobParametersValidator; 024import ca.uhn.fhir.batch2.api.IJobStepWorker; 025import ca.uhn.fhir.batch2.api.IReductionStepWorker; 026import ca.uhn.fhir.batch2.api.VoidModel; 027import ca.uhn.fhir.context.ConfigurationException; 028import ca.uhn.fhir.i18n.Msg; 029import ca.uhn.fhir.model.api.IModelJson; 030import ca.uhn.fhir.util.Logs; 031import jakarta.annotation.Nonnull; 032import jakarta.annotation.Nullable; 033import org.apache.commons.lang3.Validate; 034import org.slf4j.Logger; 035 036import java.util.ArrayList; 037import java.util.Collections; 038import java.util.List; 039import java.util.stream.Collectors; 040 041public class JobDefinition<PT extends IModelJson> { 042 private static final Logger ourLog = Logs.getBatchTroubleshootingLog(); 043 public static final int ID_MAX_LENGTH = 100; 044 045 private final String myJobDefinitionId; 046 private final int myJobDefinitionVersion; 047 private final Class<PT> myParametersType; 048 private final List<JobDefinitionStep<PT, ?, ?>> mySteps; 049 private final String myJobDescription; 050 private final IJobParametersValidator<PT> myParametersValidator; 051 private final boolean myGatedExecution; 052 private final List<String> myStepIds; 053 private final IJobCompletionHandler<PT> myCompletionHandler; 054 private final IJobCompletionHandler<PT> myErrorHandler; 055 056 /** 057 * Constructor 058 */ 059 private JobDefinition( 060 String theJobDefinitionId, 061 int theJobDefinitionVersion, 062 String theJobDescription, 063 Class<PT> theParametersType, 064 List<JobDefinitionStep<PT, ?, ?>> theSteps, 065 IJobParametersValidator<PT> theParametersValidator, 066 boolean theGatedExecution, 067 IJobCompletionHandler<PT> theCompletionHandler, 068 IJobCompletionHandler<PT> theErrorHandler) { 069 Validate.isTrue(theJobDefinitionId.length() <= ID_MAX_LENGTH, "Maximum ID length is %d", ID_MAX_LENGTH); 070 Validate.notBlank(theJobDefinitionId, "No job definition ID supplied"); 071 Validate.notBlank(theJobDescription, "No job description supplied"); 072 Validate.isTrue(theJobDefinitionVersion >= 1, "No job definition version supplied (must be >= 1)"); 073 Validate.isTrue(theSteps.size() >= 2, "At least 2 steps must be supplied"); 074 myJobDefinitionId = theJobDefinitionId; 075 myJobDefinitionVersion = theJobDefinitionVersion; 076 myJobDescription = theJobDescription; 077 mySteps = theSteps; 078 myStepIds = mySteps.stream().map(JobDefinitionStep::getStepId).collect(Collectors.toList()); 079 myParametersType = theParametersType; 080 myParametersValidator = theParametersValidator; 081 myGatedExecution = theGatedExecution; 082 myCompletionHandler = theCompletionHandler; 083 myErrorHandler = theErrorHandler; 084 } 085 086 @Nullable 087 public IJobCompletionHandler<PT> getCompletionHandler() { 088 return myCompletionHandler; 089 } 090 091 @Nullable 092 public IJobCompletionHandler<PT> getErrorHandler() { 093 return myErrorHandler; 094 } 095 096 @Nullable 097 public IJobParametersValidator<PT> getParametersValidator() { 098 return myParametersValidator; 099 } 100 101 @SuppressWarnings("unused") 102 public String getJobDescription() { 103 return myJobDescription; 104 } 105 106 /** 107 * @return Returns a unique identifier for the job definition (i.e. for the "kind" of job) 108 */ 109 public String getJobDefinitionId() { 110 return myJobDefinitionId; 111 } 112 113 /** 114 * @return Returns a unique identifier for the version of the job definition. Higher means newer but numbers have no other meaning. Must be greater than 0. 115 */ 116 public int getJobDefinitionVersion() { 117 return myJobDefinitionVersion; 118 } 119 120 /** 121 * @return Returns the parameters that this job can accept as input to create a new instance 122 */ 123 public Class<PT> getParametersType() { 124 return myParametersType; 125 } 126 127 /** 128 * @return Returns the processing steps for this job 129 */ 130 public List<JobDefinitionStep<PT, ?, ?>> getSteps() { 131 return mySteps; 132 } 133 134 /** 135 * 136 * @return Returns the stepId of the first step 137 * @throws IndexOutOfBoundsException if there is no first step 138 */ 139 public String getFirstStepId() { 140 JobDefinitionStep<PT, ?, ?> firstStep = mySteps.get(0); 141 return firstStep.getStepId(); 142 } 143 144 public boolean isGatedExecution() { 145 return myGatedExecution; 146 } 147 148 public JobDefinitionStep<?, ?, ?> getStepById(String theId) { 149 return getSteps().stream() 150 .filter(s -> s.getStepId().equals(theId)) 151 .findFirst() 152 .orElse(null); 153 } 154 155 public boolean isLastStepReduction() { 156 int stepCount = getSteps().size(); 157 return stepCount >= 1 && getSteps().get(stepCount - 1).isReductionStep(); 158 } 159 160 public int getStepIndex(String theStepId) { 161 int retVal = myStepIds.indexOf(theStepId); 162 Validate.isTrue(retVal != -1); 163 return retVal; 164 } 165 166 public static class Builder<PT extends IModelJson, NIT extends IModelJson> { 167 168 private final List<JobDefinitionStep<PT, ?, ?>> mySteps; 169 private String myJobDefinitionId; 170 private int myJobDefinitionVersion; 171 private String myJobDescription; 172 private Class<PT> myJobParametersType; 173 private Class<NIT> myNextInputType; 174 175 @Nullable 176 private IJobParametersValidator<PT> myParametersValidator; 177 178 private boolean myGatedExecution; 179 private IJobCompletionHandler<PT> myCompletionHandler; 180 private IJobCompletionHandler<PT> myErrorHandler; 181 182 Builder() { 183 mySteps = new ArrayList<>(); 184 } 185 186 Builder( 187 List<JobDefinitionStep<PT, ?, ?>> theSteps, 188 String theJobDefinitionId, 189 int theJobDefinitionVersion, 190 String theJobDescription, 191 Class<PT> theJobParametersType, 192 Class<NIT> theNextInputType, 193 @Nullable IJobParametersValidator<PT> theParametersValidator, 194 boolean theGatedExecution, 195 IJobCompletionHandler<PT> theCompletionHandler, 196 IJobCompletionHandler<PT> theErrorHandler) { 197 mySteps = theSteps; 198 myJobDefinitionId = theJobDefinitionId; 199 myJobDefinitionVersion = theJobDefinitionVersion; 200 myJobDescription = theJobDescription; 201 myJobParametersType = theJobParametersType; 202 myNextInputType = theNextInputType; 203 myParametersValidator = theParametersValidator; 204 myGatedExecution = theGatedExecution; 205 myCompletionHandler = theCompletionHandler; 206 myErrorHandler = theErrorHandler; 207 } 208 209 /** 210 * @param theJobDefinitionId A unique identifier for the job definition (i.e. for the "kind" of job) 211 */ 212 public Builder<PT, NIT> setJobDefinitionId(String theJobDefinitionId) { 213 myJobDefinitionId = theJobDefinitionId; 214 return this; 215 } 216 217 /** 218 * @param theJobDefinitionVersion A unique identifier for the version of the job definition. Higher means newer but numbers have no other meaning. Must be greater than 0. 219 */ 220 public Builder<PT, NIT> setJobDefinitionVersion(int theJobDefinitionVersion) { 221 Validate.isTrue(theJobDefinitionVersion > 0, "theJobDefinitionVersion must be > 0"); 222 myJobDefinitionVersion = theJobDefinitionVersion; 223 return this; 224 } 225 226 /** 227 * Adds a processing step for this job. 228 * 229 * @param theStepId A unique identifier for this step. This only needs to be unique within the scope 230 * of the individual job definition (i.e. diuplicates are fine for different jobs, or 231 * even different versions of the same job) 232 * @param theStepDescription A description of this step 233 * @param theStepWorker The worker that will actually perform this step 234 */ 235 public <OT extends IModelJson> Builder<PT, OT> addFirstStep( 236 String theStepId, 237 String theStepDescription, 238 Class<OT> theOutputType, 239 IJobStepWorker<PT, VoidModel, OT> theStepWorker) { 240 mySteps.add(new JobDefinitionStep<>( 241 theStepId, theStepDescription, theStepWorker, VoidModel.class, theOutputType)); 242 return new Builder<>( 243 mySteps, 244 myJobDefinitionId, 245 myJobDefinitionVersion, 246 myJobDescription, 247 myJobParametersType, 248 theOutputType, 249 myParametersValidator, 250 myGatedExecution, 251 myCompletionHandler, 252 myErrorHandler); 253 } 254 255 /** 256 * Adds a processing step for this job. 257 * 258 * @param theStepId A unique identifier for this step. This only needs to be unique within the scope 259 * of the individual job definition (i.e. duplicates are fine for different jobs, or 260 * even different versions of the same job) 261 * @param theStepDescription A description of this step 262 * @param theStepWorker The worker that will actually perform this step 263 */ 264 public <OT extends IModelJson> Builder<PT, OT> addIntermediateStep( 265 String theStepId, 266 String theStepDescription, 267 Class<OT> theOutputType, 268 IJobStepWorker<PT, NIT, OT> theStepWorker) { 269 mySteps.add(new JobDefinitionStep<>( 270 theStepId, theStepDescription, theStepWorker, myNextInputType, theOutputType)); 271 return new Builder<>( 272 mySteps, 273 myJobDefinitionId, 274 myJobDefinitionVersion, 275 myJobDescription, 276 myJobParametersType, 277 theOutputType, 278 myParametersValidator, 279 myGatedExecution, 280 myCompletionHandler, 281 myErrorHandler); 282 } 283 284 /** 285 * Adds a processing step for this job. 286 * 287 * @param theStepId A unique identifier for this step. This only needs to be unique within the scope 288 * of the individual job definition (i.e. diuplicates are fine for different jobs, or 289 * even different versions of the same job) 290 * @param theStepDescription A description of this step 291 * @param theStepWorker The worker that will actually perform this step 292 */ 293 public Builder<PT, VoidModel> addLastStep( 294 String theStepId, String theStepDescription, IJobStepWorker<PT, NIT, VoidModel> theStepWorker) { 295 mySteps.add(new JobDefinitionStep<>( 296 theStepId, theStepDescription, theStepWorker, myNextInputType, VoidModel.class)); 297 return new Builder<>( 298 mySteps, 299 myJobDefinitionId, 300 myJobDefinitionVersion, 301 myJobDescription, 302 myJobParametersType, 303 VoidModel.class, 304 myParametersValidator, 305 myGatedExecution, 306 myCompletionHandler, 307 myErrorHandler); 308 } 309 310 public <OT extends IModelJson> Builder<PT, OT> addFinalReducerStep( 311 String theStepId, 312 String theStepDescription, 313 Class<OT> theOutputType, 314 IReductionStepWorker<PT, NIT, OT> theStepWorker) { 315 if (!myGatedExecution) { 316 throw new ConfigurationException(Msg.code(2106) 317 + String.format("Job Definition %s has a reducer step but is not gated", myJobDefinitionId)); 318 } 319 mySteps.add(new JobDefinitionReductionStep<>( 320 theStepId, theStepDescription, theStepWorker, myNextInputType, theOutputType)); 321 return new Builder<>( 322 mySteps, 323 myJobDefinitionId, 324 myJobDefinitionVersion, 325 myJobDescription, 326 myJobParametersType, 327 theOutputType, 328 myParametersValidator, 329 myGatedExecution, 330 myCompletionHandler, 331 myErrorHandler); 332 } 333 334 public JobDefinition<PT> build() { 335 Validate.notNull(myJobParametersType, "No job parameters type was supplied"); 336 return new JobDefinition<>( 337 myJobDefinitionId, 338 myJobDefinitionVersion, 339 myJobDescription, 340 myJobParametersType, 341 Collections.unmodifiableList(mySteps), 342 myParametersValidator, 343 myGatedExecution, 344 myCompletionHandler, 345 myErrorHandler); 346 } 347 348 public Builder<PT, NIT> setJobDescription(String theJobDescription) { 349 myJobDescription = theJobDescription; 350 return this; 351 } 352 353 /** 354 * Sets the datatype for the parameters used by this job. This model is a 355 * {@link IModelJson} JSON serializable object. 356 * 357 * <p> 358 * <b>Validation:</b> 359 * Fields should be annotated with 360 * any appropriate <code>jakarta.validation</code> (JSR 380) annotations (e.g. 361 * {@link jakarta.validation.constraints.Min} or {@link jakarta.validation.constraints.Pattern}). 362 * In addition, if there are validation rules that are too complex to express using 363 * JSR 380, you can also specify a programmatic validator using {@link #setParametersValidator(IJobParametersValidator)}. 364 * </p> 365 * <p> 366 * Any fields that contain sensitive data (e.g. passwords) that should not be 367 * provided back to the end user must be marked with {@link ca.uhn.fhir.model.api.annotation.PasswordField} 368 * as well. 369 * </p> 370 * 371 * @see ca.uhn.fhir.model.api.annotation.PasswordField 372 * @see jakarta.validation.constraints 373 * @see JobDefinition.Builder#setParametersValidator(IJobParametersValidator) 374 */ 375 @SuppressWarnings("unchecked") 376 public <NPT extends IModelJson> Builder<NPT, NIT> setParametersType(@Nonnull Class<NPT> theJobParametersType) { 377 Validate.notNull(theJobParametersType, "theJobParametersType must not be null"); 378 Validate.isTrue( 379 myJobParametersType == null, 380 "Can not supply multiple parameters types, already have: %s", 381 myJobParametersType); 382 myJobParametersType = (Class<PT>) theJobParametersType; 383 return (Builder<NPT, NIT>) this; 384 } 385 386 /** 387 * Supplies a programmatic job parameters validator. Note that as much as possible, 388 * JSR 380 annotations should be used for validation. This method is provided only 389 * to satisfy rules that are too complex to be expressed using JSR 380. 390 * 391 * @param theParametersValidator The validator (must not be null. Do not call this method at all if you do not want a parameters validator). 392 */ 393 public Builder<PT, NIT> setParametersValidator(@Nonnull IJobParametersValidator<PT> theParametersValidator) { 394 Validate.notNull(theParametersValidator, "theParametersValidator must not be null"); 395 Validate.isTrue( 396 myParametersValidator == null, 397 "Can not supply multiple parameters validators. Already have: %s", 398 myParametersValidator); 399 myParametersValidator = theParametersValidator; 400 return this; 401 } 402 403 /** 404 * If this is set, the framework will wait for all work chunks to be 405 * processed for an individual step before moving on to beginning 406 * processing on the next step. Otherwise, processing on subsequent 407 * steps may begin as soon as any data has been produced. 408 * <p> 409 * This is useful in a few cases: 410 * <ul> 411 * <li> 412 * If there are potential constraint issues, e.g. data being 413 * written by the third step depends on all data from the 414 * second step already being written 415 * </li> 416 * <li> 417 * If multiple steps require expensive database queries, it may 418 * reduce the chances of timeouts to ensure that they are run 419 * discretely. 420 * </li> 421 * </ul> 422 * </p> 423 * <p> 424 * Setting this mode means the job may take longer, since it will 425 * rely on a polling mechanism to determine that one step is 426 * complete before beginning any processing for the next step. 427 * </p> 428 */ 429 public Builder<PT, NIT> gatedExecution() { 430 myGatedExecution = true; 431 return this; 432 } 433 434 /** 435 * Supplies an optional callback that will be invoked when the job is complete 436 */ 437 public Builder<PT, NIT> completionHandler(IJobCompletionHandler<PT> theCompletionHandler) { 438 Validate.isTrue(myCompletionHandler == null, "Can not supply multiple completion handlers"); 439 myCompletionHandler = theCompletionHandler; 440 return this; 441 } 442 443 /** 444 * Supplies an optional callback that will be invoked if the job fails 445 */ 446 public Builder<PT, NIT> errorHandler(IJobCompletionHandler<PT> theErrorHandler) { 447 Validate.isTrue(myErrorHandler == null, "Can not supply multiple error handlers"); 448 myErrorHandler = theErrorHandler; 449 return this; 450 } 451 } 452 453 public static Builder<IModelJson, VoidModel> newBuilder() { 454 return new Builder<>(); 455 } 456}