Implant vid-app-common org.onap.vid.job (main and test)
[vid.git] / vid-app-common / src / main / java / org / onap / vid / job / command / ResourceCommand.kt
1 /*-
2  * ============LICENSE_START=======================================================
3  * VID
4  * ================================================================================
5  * Copyright (C) 2017 - 2019 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.vid.job.command
22
23
24 import com.fasterxml.jackson.module.kotlin.convertValue
25 import org.onap.portalsdk.core.logging.logic.EELFLoggerDelegate
26 import org.onap.vid.changeManagement.RequestDetailsWrapper
27 import org.onap.vid.exceptions.AbortingException
28 import org.onap.vid.exceptions.TryAgainException
29 import org.onap.vid.job.*
30 import org.onap.vid.job.Job.JobStatus
31 import org.onap.vid.job.impl.JobSharedData
32 import org.onap.vid.model.Action
33 import org.onap.vid.model.RequestReferencesContainer
34 import org.onap.vid.model.serviceInstantiation.BaseResource
35 import org.onap.vid.mso.RestMsoImplementation
36 import org.onap.vid.utils.JACKSON_OBJECT_MAPPER
37 import org.onap.vid.utils.getEnumFromMapOfStrings
38 import org.springframework.http.HttpMethod
39 import java.util.*
40
41
42 const val INTERNAL_STATE = "internalState"
43 const val ACTION_PHASE = "actionPhase"
44 const val CHILD_JOBS = "childJobs"
45 const val MSO_RESOURCE_ID = "msoResourceIds"
46 const val CUMULATIVE_STATUS = "cumulativeStatus"
47
48 enum class InternalState constructor(val immediate:Boolean=false) {
49     INITIAL,
50     CREATING_CHILDREN(true),
51     WATCHING,
52     DELETE_MYSELF,
53     CREATE_MYSELF,
54     IN_PROGRESS,
55     TERMINAL,
56     RESUME_MYSELF,
57     REPLACE_MYSELF,
58 }
59
60 data class NextInternalState(val nextActionPhase: Action, val nextInternalState: InternalState)
61
62
63 data class MsoRestCallPlan(
64         val httpMethod: HttpMethod,
65         val path: String,
66         val payload: Optional<RequestDetailsWrapper<out Any>>,
67         val userId: Optional<String>,
68         val actionDescription: String
69 )
70
71 abstract class ResourceCommand(
72         protected val restMso: RestMsoImplementation,
73         protected val inProgressStatusService: InProgressStatusService,
74         protected val msoResultHandlerService: MsoResultHandlerService,
75         protected val watchChildrenJobsBL: WatchChildrenJobsBL,
76         private val jobsBrokerService: JobsBrokerService,
77         private val jobAdapter: JobAdapter
78         ) : CommandBase(), JobCommand {
79
80     companion object {
81         private val Logger = EELFLoggerDelegate.getLogger(ResourceCommand::class.java)
82     }
83
84     abstract fun createChildren():JobStatus
85
86     abstract fun planCreateMyselfRestCall(commandParentData: CommandParentData, request: JobAdapter.AsyncJobRequest, userId: String, testApi: String?): MsoRestCallPlan
87
88     abstract fun planDeleteMyselfRestCall(commandParentData: CommandParentData, request: JobAdapter.AsyncJobRequest, userId: String): MsoRestCallPlan
89
90     private val commandByInternalState: Map<InternalState, () -> JobStatus> = hashMapOf(
91             Pair(InternalState.CREATING_CHILDREN, ::createChildren),
92             Pair(InternalState.WATCHING, ::watchChildren),
93             Pair(InternalState.CREATE_MYSELF, ::createMyself),
94             Pair(InternalState.RESUME_MYSELF, ::resumeMyself),
95             Pair(InternalState.DELETE_MYSELF, ::deleteMyself),
96             Pair(InternalState.IN_PROGRESS, ::inProgress),
97             Pair(InternalState.REPLACE_MYSELF, ::replaceMyself)
98     )
99
100     private lateinit var internalState:InternalState
101     protected lateinit var actionPhase: Action
102     protected var commandParentData: CommandParentData = CommandParentData()
103     protected var msoResourceIds: MsoResourceIds = EMPTY_MSO_RESOURCE_ID
104     protected var childJobs:List<String> = emptyList()
105     private lateinit var cumulativeStatus:JobStatus
106
107
108     override fun call(): NextCommand {
109         var jobStatus:JobStatus = if (internalState!=InternalState.TERMINAL) invokeCommand() else cumulativeStatus
110         jobStatus = comulateStatusAndUpdatePropertyIfFinal(jobStatus)
111
112         try {
113             Logger.debug("job: ${this.javaClass.simpleName} ${sharedData.jobUuid} $actionPhase ${getActionType()} $internalState $jobStatus $childJobs")
114         } catch (e:Exception) { /* do nothing. Just failed to log...*/}
115
116         if (shallStopJob(jobStatus)) {
117             onFinal(jobStatus)
118             return NextCommand(jobStatus)
119         }
120
121         val (nextActionPhase, nextInternalState) = calcNextInternalState(jobStatus, internalState, actionPhase)
122         Logger.debug("next state for job ${sharedData.jobUuid} is $nextInternalState")
123         actionPhase = nextActionPhase
124         internalState = nextInternalState
125
126         if (internalState==InternalState.TERMINAL) {
127             onFinal(jobStatus)
128             return NextCommand(jobStatus)
129         }
130
131         jobStatus = getExternalInProgressStatus()
132         Logger.debug("next status for job ${sharedData.jobUuid} is $jobStatus")
133 //        if (internalState.immediate) return call() //shortcut instead of execute another command
134         return NextCommand(jobStatus, this)
135     }
136
137     //we want to stop in faliures, except for service witn no action, since service with no action trigger 2 phases (delete and create)
138     protected fun shallStopJob(jobStatus: JobStatus) =
139             jobStatus.isFailure && !(isServiceCommand() && getActionType()==Action.None)
140
141     //this method is used to expose the job status after successful completion of current state
142     //should be override by subclass (like ServiceCommand) that need to return other default job status
143     protected open fun getExternalInProgressStatus() = JobStatus.RESOURCE_IN_PROGRESS
144
145     private fun invokeCommand(): JobStatus {
146         return try {
147             commandByInternalState.getOrDefault(internalState, ::throwIllegalState).invoke()
148         }
149         catch (exception: TryAgainException) {
150             Logger.warn("caught TryAgainException. Set job status to IN_PROGRESS")
151             JobStatus.IN_PROGRESS
152         }
153         catch (exception: AbortingException) {
154             Logger.error("caught AbortingException. Set job status to FAILED")
155             JobStatus.FAILED;
156         }
157     }
158
159     private fun throwIllegalState():JobStatus {
160              throw IllegalStateException("can't find action for pashe $actionPhase and state $internalState")
161     }
162
163     private fun calcNextInternalState(jobStatus: JobStatus, internalState: InternalState, actionPhase: Action): NextInternalState {
164
165         val nextInternalState = when (actionPhase) {
166             Action.Delete -> calcNextStateDeletePhase(jobStatus, internalState)
167             Action.Create -> calcNextStateCreatePhase(jobStatus, internalState)
168             else -> InternalState.TERMINAL
169         }
170
171         if (nextInternalState == InternalState.TERMINAL
172                 && actionPhase == Action.Delete
173                 && isServiceCommand()) {
174             // Loop over to "Create" phase
175             return NextInternalState(Action.Create, InternalState.INITIAL)
176         }
177
178         return NextInternalState(actionPhase, nextInternalState)
179
180     }
181
182     //no need to refer to failed (final) states here
183     //This method is called only for non final states or COMPLETED
184     protected fun calcNextStateDeletePhase(jobStatus: JobStatus, internalState: InternalState): InternalState {
185         return when (internalState) {
186
187             InternalState.CREATING_CHILDREN -> InternalState.WATCHING
188
189             InternalState.WATCHING -> {
190                 when {
191                     !jobStatus.isFinal -> InternalState.WATCHING
192                     isNeedToDeleteMyself() -> InternalState.DELETE_MYSELF
193                     else -> InternalState.TERMINAL
194                 }
195             }
196
197             InternalState.DELETE_MYSELF -> InternalState.IN_PROGRESS
198
199             InternalState.IN_PROGRESS -> {
200                 if (jobStatus == JobStatus.COMPLETED) InternalState.TERMINAL else InternalState.IN_PROGRESS
201             }
202
203             else -> InternalState.TERMINAL
204         }
205     }
206
207     protected fun calcNextStateCreatePhase(jobStatus: JobStatus, internalState: InternalState): InternalState {
208         return when (internalState) {
209
210             InternalState.CREATE_MYSELF -> when (jobStatus) {
211                 JobStatus.IN_PROGRESS -> InternalState.CREATE_MYSELF
212                 else -> InternalState.IN_PROGRESS
213             }
214
215             InternalState.RESUME_MYSELF -> when (jobStatus) {
216                 JobStatus.IN_PROGRESS -> InternalState.RESUME_MYSELF
217                 else -> InternalState.IN_PROGRESS
218             }
219
220             InternalState.REPLACE_MYSELF -> when (jobStatus) {
221                 JobStatus.IN_PROGRESS -> InternalState.REPLACE_MYSELF
222                 else -> InternalState.IN_PROGRESS
223             }
224
225             InternalState.IN_PROGRESS -> {
226                 when {
227                     jobStatus != JobStatus.COMPLETED -> InternalState.IN_PROGRESS
228                     isDescendantHasAction(Action.Create) -> InternalState.CREATING_CHILDREN
229                     isDescendantHasAction(Action.Replace) -> InternalState.CREATING_CHILDREN
230                     else -> InternalState.TERMINAL
231                 }
232             }
233
234             InternalState.CREATING_CHILDREN -> InternalState.WATCHING
235
236             InternalState.WATCHING -> {
237                 when {
238                     !jobStatus.isFinal -> InternalState.WATCHING
239                     else -> InternalState.TERMINAL
240                 }
241             }
242
243             else -> InternalState.TERMINAL
244         }
245     }
246
247     override fun getData(): Map<String, Any?> {
248         return mapOf(
249                 ACTION_PHASE to actionPhase,
250                 INTERNAL_STATE to internalState,
251                 MSO_RESOURCE_ID to msoResourceIds,
252                 CHILD_JOBS to childJobs,
253                 CUMULATIVE_STATUS to cumulativeStatus
254         ) + commandParentData.parentData
255     }
256
257     override fun init(sharedData: JobSharedData, commandData: Map<String, Any>): ResourceCommand {
258         init(sharedData)
259         val resourceIdsRaw:Any? = commandData[MSO_RESOURCE_ID]
260         commandParentData.initParentData(commandData)
261         msoResourceIds =
262                 if (resourceIdsRaw != null) JACKSON_OBJECT_MAPPER.convertValue(resourceIdsRaw)
263                 else EMPTY_MSO_RESOURCE_ID
264
265         childJobs = JACKSON_OBJECT_MAPPER.convertValue(commandData.getOrDefault(CHILD_JOBS, emptyList<String>()))
266         cumulativeStatus = getEnumFromMapOfStrings(commandData, CUMULATIVE_STATUS, JobStatus.COMPLETED_WITH_NO_ACTION)
267         actionPhase = getEnumFromMapOfStrings(commandData, ACTION_PHASE, Action.Delete)
268         internalState = calcInitialState(commandData, actionPhase)
269         return this
270     }
271
272     fun calcInitialState(commandData: Map<String, Any>, phase: Action):InternalState {
273         val status:InternalState = getEnumFromMapOfStrings(commandData, INTERNAL_STATE, InternalState.INITIAL)
274         if (status == InternalState.INITIAL) {
275             onInitial(phase)
276             return when (phase) {
277                 Action.Delete -> when {
278                     isDescendantHasAction(phase) -> InternalState.CREATING_CHILDREN
279                     isNeedToDeleteMyself() -> InternalState.DELETE_MYSELF
280                     else -> InternalState.TERMINAL
281                 }
282                 Action.Create -> when {
283                     isNeedToCreateMyself() -> InternalState.CREATE_MYSELF
284                     isNeedToResumeMySelf() -> InternalState.RESUME_MYSELF
285                     isNeedToReplaceMySelf() -> InternalState.REPLACE_MYSELF
286                     isDescendantHasAction(phase) -> InternalState.CREATING_CHILDREN
287                     isDescendantHasAction(Action.Replace) -> InternalState.CREATING_CHILDREN
288                     else -> InternalState.TERMINAL
289                 }
290                 else -> throw IllegalStateException("state $internalState is not supported yet")
291             }
292         }
293         return status
294     }
295
296     //command may override it in order to do something while init state
297     protected open fun onInitial(phase: Action) {
298         //do nothing
299     }
300
301     //command may override it in order to do something while final status
302     protected open fun onFinal(jobStatus: JobStatus) {
303         //do nothing
304     }
305
306     protected open fun getRequest(): BaseResource {
307         return sharedData.request as BaseResource
308     }
309
310     protected open fun getActionType(): Action {
311         return getRequest().action
312     }
313
314     protected open fun isServiceCommand(): Boolean = false
315
316     protected open fun isNeedToDeleteMyself(): Boolean = getActionType() == Action.Delete
317
318     protected open fun isNeedToCreateMyself(): Boolean = getActionType() == Action.Create
319
320     protected open fun isNeedToResumeMySelf(): Boolean = getActionType() == Action.Resume
321
322     protected open fun isNeedToReplaceMySelf(): Boolean = false
323
324     protected open fun inProgress(): JobStatus {
325         val requestId:String = msoResourceIds.requestId;
326         return try {
327             val jobStatus = inProgressStatusService.call(getExpiryChecker(), sharedData, requestId)
328             handleInProgressStatus(jobStatus)
329         } catch (e: javax.ws.rs.ProcessingException) {
330             // Retry when we can't connect MSO during getStatus
331             Logger.error(EELFLoggerDelegate.errorLogger, "Cannot get orchestration status for {}, will retry: {}", requestId, e, e)
332             JobStatus.IN_PROGRESS;
333         } catch (e: InProgressStatusService.BadResponseFromMso) {
334             inProgressStatusService.handleFailedMsoResponse(sharedData.jobUuid, requestId, e.msoResponse)
335             JobStatus.IN_PROGRESS
336         } catch (e: RuntimeException) {
337             Logger.error(EELFLoggerDelegate.errorLogger, "Cannot get orchestration status for {}, stopping: {}", requestId, e, e)
338             JobStatus.STOPPED
339         }
340     }
341
342     fun createMyself(): JobStatus {
343         val createMyselfCommand = planCreateMyselfRestCall(commandParentData, sharedData.request, sharedData.userId, sharedData.testApi)
344         return executeAndHandleMsoInstanceRequest(createMyselfCommand)
345     }
346
347     protected open fun resumeMyself(): JobStatus {
348         throw NotImplementedError("Resume is not implemented for this command " + this.javaClass)
349     }
350
351     protected open fun replaceMyself(): JobStatus {
352         throw NotImplementedError("Replace is not implemented for this command " + this.javaClass)
353     }
354
355     fun deleteMyself(): JobStatus {
356         val deleteMyselfCommand = planDeleteMyselfRestCall(commandParentData, sharedData.request, sharedData.userId)
357         return executeAndHandleMsoInstanceRequest(deleteMyselfCommand)
358     }
359
360     protected fun executeAndHandleMsoInstanceRequest(restCallPlan: MsoRestCallPlan): JobStatus {
361         val msoResponse = restMso.restCall(
362                 restCallPlan.httpMethod,
363                 RequestReferencesContainer::class.java,
364                 restCallPlan.payload.orElse(null),
365                 restCallPlan.path,
366                 restCallPlan.userId
367         )
368
369         val msoResult = if (isServiceCommand()) {
370             msoResultHandlerService.handleRootResponse(sharedData, msoResponse)
371         } else {
372             msoResultHandlerService.handleResponse(sharedData, msoResponse, restCallPlan.actionDescription)
373         }
374
375         this.msoResourceIds = msoResult.msoResourceIds
376         return msoResult.jobStatus
377     }
378
379     protected open fun getExpiryChecker(): ExpiryChecker = ExpiryChecker {false}
380
381     protected open fun handleInProgressStatus(jobStatus: JobStatus): JobStatus {
382         return if (jobStatus == JobStatus.PAUSE) JobStatus.IN_PROGRESS else jobStatus
383     }
384
385     protected open fun watchChildren():JobStatus {
386         return watchChildrenJobsBL.retrieveChildrenJobsStatus(childJobs)
387     }
388
389     protected fun comulateStatusAndUpdatePropertyIfFinal(internalStateStatus: JobStatus): JobStatus {
390         val status = watchChildrenJobsBL.cumulateJobStatus(internalStateStatus, cumulativeStatus)
391
392         //we want to update cumulativeStatus only for final status
393         if (status.isFinal) {
394             cumulativeStatus = status;
395         }
396
397         return status
398     }
399
400     protected fun buildDataForChild(request: BaseResource, actionPhase: Action): Map<String, Any> {
401         addMyselfToChildrenData(commandParentData, request)
402         commandParentData.setActionPhase(actionPhase)
403         return commandParentData.parentData
404     }
405
406     protected open fun addMyselfToChildrenData(commandParentData: CommandParentData, request: BaseResource) {
407         // Nothing by default
408     }
409
410     protected open fun isDescendantHasAction(phase:Action):Boolean = isDescendantHasAction(getRequest(), phase, true )
411
412
413     @JvmOverloads
414     fun isDescendantHasAction(request: BaseResource, phase: Action, isFirstLevel:Boolean=true): Boolean {
415         if (!isFirstLevel && request.action == phase) {
416             return true;
417         }
418
419         return request.children.map {this.isDescendantHasAction(it, phase, false)}.any {it}
420     }
421
422     protected fun getActualInstanceId(request: BaseResource):String =
423             if (getActionType() == Action.Create) msoResourceIds.instanceId else request.instanceId
424
425
426     protected fun pushChildrenJobsToBroker(children:Collection<BaseResource>,
427                                            dataForChild: Map<String, Any>,
428                                            jobType: JobType?=null): List<String> {
429         var counter = 0;
430         return  children
431                 .map {Pair(it, counter++)}
432                 .map { jobAdapter.createChildJob(jobType ?: it.first.jobType, it.first, sharedData, dataForChild, it.second) }
433                 .map { jobsBrokerService.add(it) }
434                 .map { it.toString() }
435     }
436
437 }
438
439
440