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}