2 * Copyright © 2017-2018 AT&T Intellectual Property.
3 * Modifications Copyright © 2019 Bell Canada.
4 * Modifications Copyright © 2019 IBM.
5 * Modifications Copyright © 2019 Orange.
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
11 * http://www.apache.org/licenses/LICENSE-2.0
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.
20 package org.onap.ccsdk.cds.blueprintsprocessor.designer.api.handler
22 import org.onap.ccsdk.cds.blueprintsprocessor.db.primary.domain.BlueprintModel
23 import org.onap.ccsdk.cds.blueprintsprocessor.db.primary.domain.BlueprintModelSearch
24 import org.onap.ccsdk.cds.blueprintsprocessor.db.primary.repository.BlueprintModelContentRepository
25 import org.onap.ccsdk.cds.blueprintsprocessor.db.primary.repository.BlueprintModelRepository
26 import org.onap.ccsdk.cds.blueprintsprocessor.db.primary.repository.BlueprintModelSearchRepository
27 import org.onap.ccsdk.cds.blueprintsprocessor.designer.api.DesignerApiDomains
28 import org.onap.ccsdk.cds.blueprintsprocessor.designer.api.BootstrapRequest
29 import org.onap.ccsdk.cds.blueprintsprocessor.designer.api.WorkFlowSpecRequest
30 import org.onap.ccsdk.cds.blueprintsprocessor.designer.api.WorkFlowSpecResponse
31 import org.onap.ccsdk.cds.blueprintsprocessor.designer.api.WorkFlowData
32 import org.onap.ccsdk.cds.blueprintsprocessor.designer.api.WorkFlowsResponse
33 import org.onap.ccsdk.cds.blueprintsprocessor.designer.api.load.BluePrintDatabaseLoadService
34 import org.onap.ccsdk.cds.blueprintsprocessor.designer.api.utils.BluePrintEnhancerUtils
35 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintException
36 import org.onap.ccsdk.cds.controllerblueprints.core.logger
37 import org.onap.ccsdk.cds.controllerblueprints.core.httpProcessorException
38 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
39 import org.onap.ccsdk.cds.controllerblueprints.core.updateErrorMessage
40 import org.onap.ccsdk.cds.controllerblueprints.core.normalizedPathName
41 import org.onap.ccsdk.cds.controllerblueprints.core.normalizedFile
42 import org.onap.ccsdk.cds.controllerblueprints.core.deleteNBDir
43 import org.onap.ccsdk.cds.controllerblueprints.core.config.BluePrintLoadConfiguration
44 import org.onap.ccsdk.cds.controllerblueprints.core.data.DataType
45 import org.onap.ccsdk.cds.controllerblueprints.core.data.PropertyDefinition
46 import org.onap.ccsdk.cds.controllerblueprints.core.interfaces.BluePrintCatalogService
47 import org.onap.ccsdk.cds.controllerblueprints.core.interfaces.BluePrintEnhancerService
48 import org.onap.ccsdk.cds.controllerblueprints.core.scripts.BluePrintCompileCache
49 import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintContext
50 import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintFileUtils
51 import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintMetadataUtils
52 import org.onap.ccsdk.cds.error.catalog.core.ErrorCatalogCodes
53 import org.onap.ccsdk.cds.error.catalog.core.utils.errorCauseOrDefault
54 import org.onap.ccsdk.cds.error.catalog.core.utils.errorMessageOrDefault
55 import org.springframework.core.io.ByteArrayResource
56 import org.springframework.core.io.Resource
57 import org.springframework.data.domain.Page
58 import org.springframework.data.domain.PageRequest
59 import org.springframework.data.domain.Pageable
60 import org.springframework.http.HttpHeaders
61 import org.springframework.http.MediaType
62 import org.springframework.http.ResponseEntity
63 import org.springframework.http.codec.multipart.FilePart
64 import org.springframework.stereotype.Service
65 import org.springframework.transaction.annotation.Transactional
66 import java.io.IOException
70 * BlueprintModelHandler Purpose: Handler service to handle the request from BlurPrintModelRest
72 * @author Brinda Santh
77 open class BluePrintModelHandler(
78 private val bluePrintDatabaseLoadService: BluePrintDatabaseLoadService,
79 private val blueprintsProcessorCatalogService: BluePrintCatalogService,
80 private val bluePrintLoadConfiguration: BluePrintLoadConfiguration,
81 private val blueprintModelSearchRepository: BlueprintModelSearchRepository,
82 private val blueprintModelRepository: BlueprintModelRepository,
83 private val blueprintModelContentRepository: BlueprintModelContentRepository,
84 private val bluePrintEnhancerService: BluePrintEnhancerService
87 private val log = logger(BluePrintModelHandler::class)
89 open suspend fun bootstrapBlueprint(bootstrapRequest: BootstrapRequest) {
91 "Bootstrap request with type load(${bootstrapRequest.loadModelType}), " +
92 "resource dictionary load(${bootstrapRequest.loadResourceDictionary}) and " +
93 "cba load(${bootstrapRequest.loadCBA})"
95 if (bootstrapRequest.loadModelType) {
96 bluePrintDatabaseLoadService.initModelTypes()
98 if (bootstrapRequest.loadResourceDictionary) {
99 bluePrintDatabaseLoadService.initResourceDictionary()
101 if (bootstrapRequest.loadCBA) {
102 bluePrintDatabaseLoadService.initBluePrintCatalog()
106 @Throws(BluePrintException::class)
107 open suspend fun prepareWorkFlowSpec(req: WorkFlowSpecRequest):
108 WorkFlowSpecResponse {
109 val basePath = blueprintsProcessorCatalogService.getFromDatabase(req
110 .blueprintName, req.version)
111 log.info("blueprint base path $basePath")
113 val blueprintContext = BluePrintMetadataUtils.getBluePrintContext(basePath.toString())
114 val workFlow = blueprintContext.workflowByName(req.workflowName)
116 val wfRes = WorkFlowSpecResponse()
117 wfRes.blueprintName = req.blueprintName
118 wfRes.version = req.version
120 val workFlowData = WorkFlowData()
121 workFlowData.workFlowName = req.workflowName
122 workFlowData.inputs = workFlow.inputs
123 workFlowData.outputs = workFlow.outputs
125 if (workFlow.inputs != null) {
126 for ((k, v) in workFlow.inputs!!) {
127 addPropertyInfo(v, blueprintContext, wfRes)
131 if (workFlow.outputs != null) {
132 for ((k, v) in workFlow.outputs!!) {
133 addPropertyInfo(v, blueprintContext, wfRes)
137 wfRes.workFlowData = workFlowData
141 private fun addPropertyInfo(prop: PropertyDefinition, ctx: BluePrintContext, res: WorkFlowSpecResponse) {
142 addDataType(prop.type, ctx, res)
143 if (prop.entrySchema != null && prop.entrySchema!!.type != null) {
144 addDataType(prop.entrySchema!!.type, ctx, res)
148 private fun addDataType(name: String, ctx: BluePrintContext, res: WorkFlowSpecResponse) {
149 var data = ctx.dataTypeByName(name)
151 res.dataTypes?.put(name, data)
152 addParentDataType(data, ctx, res)
156 private fun addParentDataType(data: DataType, ctx: BluePrintContext, res: WorkFlowSpecResponse) {
157 if (data.properties != null) {
158 for ((k, v) in data.properties!!) {
159 addPropertyInfo(v, ctx, res)
164 @Throws(BluePrintException::class)
165 open suspend fun getWorkflowNames(name: String, version: String): WorkFlowsResponse {
166 val basePath = blueprintsProcessorCatalogService.getFromDatabase(
168 log.info("blueprint base path $basePath")
170 var res = WorkFlowsResponse()
171 res.blueprintName = name
172 res.version = version
174 val blueprintContext = BluePrintMetadataUtils.getBluePrintContext(
176 if (blueprintContext.workflows() != null) {
177 res.workflows = blueprintContext.workflows()!!.keys
183 * This is a getAllBlueprintModel method to retrieve all the BlueprintModel in Database
185 * @return List<BlueprintModelSearch> list of the controller blueprint archives
186 </BlueprintModelSearch> */
187 open fun allBlueprintModel(): List<BlueprintModelSearch> {
188 return blueprintModelSearchRepository.findAll()
192 * This is a getAllBlueprintModel method to retrieve all the BlueprintModel in Database
194 * @return List<BlueprintModelSearch> list of the controller blueprint archives
195 </BlueprintModelSearch> */
196 open fun allBlueprintModel(pageRequest: Pageable): Page<BlueprintModelSearch> {
197 return blueprintModelSearchRepository.findAll(pageRequest)
201 * This is a saveBlueprintModel method
203 * @param filePart filePart
204 * @return Mono<BlueprintModelSearch>
205 * @throws BluePrintException BluePrintException
206 </BlueprintModelSearch> */
207 @Throws(BluePrintException::class)
208 open suspend fun saveBlueprintModel(filePart: FilePart): BlueprintModelSearch {
210 return upload(filePart, false)
211 } catch (e: IOException) {
212 throw httpProcessorException(ErrorCatalogCodes.IO_FILE_INTERRUPT, DesignerApiDomains.DESIGNER_API,
213 "Error in Save CBA: ${e.message}", e.errorCauseOrDefault())
218 * This is a searchBlueprintModels method
221 * @return List<BlueprintModelSearch>
222 </BlueprintModelSearch> */
223 open fun searchBlueprintModels(tags: String): List<BlueprintModelSearch> {
224 return blueprintModelSearchRepository.findByTagsContainingIgnoreCase(tags)
228 * This is a getBlueprintModelSearchByNameAndVersion method
231 * @param version version
232 * @return BlueprintModelSearch
233 * @throws BluePrintException BluePrintException
235 @Throws(BluePrintException::class)
236 open fun getBlueprintModelSearchByNameAndVersion(name: String, version: String): BlueprintModelSearch? {
237 return blueprintModelSearchRepository.findByArtifactNameAndArtifactVersion(name, version)
238 /*?: throw BluePrintException(
239 ErrorCode.RESOURCE_NOT_FOUND.value,
240 String.format(BLUEPRINT_MODEL_NAME_VERSION_FAILURE_MSG, name, version)
245 * This is a downloadBlueprintModelFileByNameAndVersion method to download a Blueprint by Name and Version
248 * @param version version
249 * @return ResponseEntity<Resource>
250 * @throws BluePrintException BluePrintException
252 @Throws(BluePrintException::class)
253 open fun downloadBlueprintModelFileByNameAndVersion(
256 ): ResponseEntity<Resource> {
258 val archiveByteArray = download(name, version)
259 val fileName = "${name}_$version.zip"
260 return prepareResourceEntity(fileName, archiveByteArray)
261 } catch (e: BluePrintProcessorException) {
262 e.http(ErrorCatalogCodes.RESOURCE_NOT_FOUND)
263 val errorMsg = "Error while downloading the CBA file by Blueprint Name ($name) and Version ($version)."
264 throw e.updateErrorMessage(DesignerApiDomains.DESIGNER_API, errorMsg,
265 "Wrong resource definition or resolution failed.")
270 * This is a downloadBlueprintModelFile method to find the target file to download and return a file resource
272 * @return ResponseEntity<Resource>
273 * @throws BluePrintException BluePrintException
275 @Throws(BluePrintException::class)
276 open fun downloadBlueprintModelFile(id: String): ResponseEntity<Resource> {
277 val blueprintModel: BlueprintModel
279 blueprintModel = getBlueprintModel(id)
280 } catch (e: BluePrintException) {
281 throw httpProcessorException(ErrorCatalogCodes.RESOURCE_NOT_FOUND, DesignerApiDomains.DESIGNER_API,
282 "Error while downloading the CBA file: couldn't get blueprint modelby ID ($id)",
283 e.errorCauseOrDefault())
286 val fileName = "${blueprintModel.artifactName}_${blueprintModel.artifactVersion}.zip"
287 val file = blueprintModel.blueprintModelContent?.content
288 ?: throw httpProcessorException(ErrorCatalogCodes.RESOURCE_NOT_FOUND, DesignerApiDomains.DESIGNER_API,
289 "Error while downloading the CBA file: couldn't get model content")
290 return prepareResourceEntity(fileName, file)
294 * @return ResponseEntity<Resource>
296 private fun prepareResourceEntity(fileName: String, file: ByteArray): ResponseEntity<Resource> {
297 return ResponseEntity.ok()
298 .contentType(MediaType.parseMediaType("text/plain"))
299 .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"$fileName\"")
300 .body(ByteArrayResource(file))
304 * This is a getBlueprintModel method
307 * @return BlueprintModel
308 * @throws BluePrintException BluePrintException
310 @Throws(BluePrintException::class)
311 open fun getBlueprintModel(id: String): BlueprintModel {
312 val blueprintModel: BlueprintModel
313 val dbBlueprintModel = blueprintModelRepository.findById(id)
314 if (dbBlueprintModel.isPresent) {
315 blueprintModel = dbBlueprintModel.get()
317 val msg = String.format(BLUEPRINT_MODEL_ID_FAILURE_MSG, id)
318 throw httpProcessorException(ErrorCatalogCodes.RESOURCE_NOT_FOUND, DesignerApiDomains.DESIGNER_API, msg)
320 return blueprintModel
324 * This is a getBlueprintModelByNameAndVersion method
327 * @param version version
328 * @return BlueprintModel
329 * @throws BluePrintException BluePrintException
331 @Throws(BluePrintException::class)
332 open fun getBlueprintModelByNameAndVersion(name: String, version: String): BlueprintModel {
333 val blueprintModel = blueprintModelRepository
334 .findByArtifactNameAndArtifactVersion(name, version)
335 if (blueprintModel != null) {
336 return blueprintModel
338 val msg = String.format(BLUEPRINT_MODEL_NAME_VERSION_FAILURE_MSG, name, version)
339 throw httpProcessorException(ErrorCatalogCodes.RESOURCE_NOT_FOUND, DesignerApiDomains.DESIGNER_API, msg)
344 * This is a getBlueprintModelSearch method
347 * @return BlueprintModelSearch
348 * @throws BluePrintException BluePrintException
350 @Throws(BluePrintException::class)
351 open fun getBlueprintModelSearch(id: String): BlueprintModelSearch {
352 return blueprintModelSearchRepository.findById(id)
353 ?: throw httpProcessorException(ErrorCatalogCodes.RESOURCE_NOT_FOUND, DesignerApiDomains.DESIGNER_API,
354 String.format(BLUEPRINT_MODEL_ID_FAILURE_MSG, id))
358 * This is a searchBluePrintModelsByKeyWord method to retrieve specific BlueprintModel in Database
359 * where keyword equals updatedBy or tags or artifcat name or artifcat version or artifact type
360 * @author Shaaban Ebrahim
363 * @return List<BlueprintModelSearch> list of the controller blueprint
364 </BlueprintModelSearch> */
365 open fun searchBluePrintModelsByKeyWord(keyWord: String): List<BlueprintModelSearch> {
366 return blueprintModelSearchRepository.findByUpdatedByOrTagsOrOrArtifactNameOrOrArtifactVersionOrArtifactType(
367 keyWord, keyWord, keyWord, keyWord, keyWord
372 * This is a searchBluePrintModelsByKeyWordPagebale method to retrieve specific BlueprintModel in Database
373 * where keyword equals updatedBy or tags or artifcat name or artifcat version or artifact type and pageable
374 * @author Shaaban Ebrahim
377 * @return List<BlueprintModelSearch> list of the controller blueprint
378 </BlueprintModelSearch> */
379 open fun searchBluePrintModelsByKeyWordPaged(keyWord: String, pageRequest: PageRequest): Page<BlueprintModelSearch> {
380 return blueprintModelSearchRepository.findByUpdatedByContainingIgnoreCaseOrTagsContainingIgnoreCaseOrArtifactNameContainingIgnoreCaseOrArtifactVersionContainingIgnoreCaseOrArtifactTypeContainingIgnoreCase(
391 * This is a deleteBlueprintModel method
394 * @throws BluePrintException BluePrintException
397 @Throws(BluePrintException::class)
398 open fun deleteBlueprintModel(id: String) {
399 val dbBlueprintModel = blueprintModelRepository.findById(id)
400 if (dbBlueprintModel.isPresent) {
401 blueprintModelContentRepository.deleteByBlueprintModel(dbBlueprintModel.get())
402 blueprintModelRepository.delete(dbBlueprintModel.get())
404 val msg = String.format(BLUEPRINT_MODEL_ID_FAILURE_MSG, id)
405 throw httpProcessorException(ErrorCatalogCodes.RESOURCE_NOT_FOUND, DesignerApiDomains.DESIGNER_API, msg)
409 open suspend fun deleteBlueprintModel(name: String, version: String) {
410 blueprintsProcessorCatalogService.deleteFromDatabase(name, version)
414 * This is a CBA enrichBlueprint method
415 * Save the Zip File in archive location and extract the cba content.
416 * Populate the Enhancement Location
417 * Enhance the CBA content
418 * Compress the Enhanced Content
419 * Return back the the compressed content back to the caller.
421 * @param filePart filePart
422 * @return ResponseEntity<Resource>
423 * @throws BluePrintException BluePrintException
425 @Throws(BluePrintException::class)
426 open suspend fun enrichBlueprint(filePart: FilePart): ResponseEntity<Resource> {
428 val enhancedByteArray = enrichBlueprintFileSource(filePart)
429 return BluePrintEnhancerUtils.prepareResourceEntity("enhanced-cba.zip", enhancedByteArray)
430 } catch (e: BluePrintProcessorException) {
431 e.http(ErrorCatalogCodes.IO_FILE_INTERRUPT)
432 val errorMsg = "Error while enhancing the CBA package."
433 throw e.updateErrorMessage(DesignerApiDomains.DESIGNER_API, errorMsg,
434 "Wrong CBA file provided, please verify and enrich Again.")
435 } catch (e: Exception) {
436 throw httpProcessorException(ErrorCatalogCodes.IO_FILE_INTERRUPT, DesignerApiDomains.DESIGNER_API,
437 "EnrichBlueprint: ${e.message}", e.errorCauseOrDefault())
442 * This is a publishBlueprintModel method to change the status published to YES
444 * @param filePart filePart
445 * @return BlueprintModelSearch
446 * @throws BluePrintException BluePrintException
448 @Throws(BluePrintException::class)
449 open suspend fun publishBlueprint(filePart: FilePart): BlueprintModelSearch {
451 return upload(filePart, true)
452 } catch (e: BluePrintProcessorException) {
453 e.http(ErrorCatalogCodes.IO_FILE_INTERRUPT)
454 val errorMsg = "Error in Publishing CBA."
455 throw e.updateErrorMessage(DesignerApiDomains.DESIGNER_API, errorMsg,
456 "Wrong CBA provided, please verify and enrich your CBA.")
457 } catch (e: Exception) {
458 throw httpProcessorException(ErrorCatalogCodes.IO_FILE_INTERRUPT, DesignerApiDomains.DESIGNER_API,
459 "Error in Publishing CBA: ${e.message}", e.errorCauseOrDefault())
463 /** Common CBA Save and Publish function for RestController and GRPC Handler, the [fileSource] may be
464 * byteArray or File Part type.*/
465 open suspend fun upload(fileSource: Any, validate: Boolean): BlueprintModelSearch {
466 val saveId = UUID.randomUUID().toString()
467 val blueprintArchive = normalizedPathName(bluePrintLoadConfiguration.blueprintArchivePath, saveId)
468 val blueprintWorking = normalizedPathName(bluePrintLoadConfiguration.blueprintWorkingPath, saveId)
470 val compressedFile = normalizedFile(blueprintArchive, "cba.zip")
472 is FilePart -> BluePrintEnhancerUtils.filePartAsFile(fileSource, compressedFile)
473 is ByteArray -> BluePrintEnhancerUtils.byteArrayAsFile(fileSource, compressedFile)
475 // Save the Copied file to Database
476 val blueprintId = blueprintsProcessorCatalogService.saveToDatabase(saveId, compressedFile, validate)
478 return blueprintModelSearchRepository.findById(blueprintId)
479 ?: throw httpProcessorException(ErrorCatalogCodes.RESOURCE_NOT_FOUND, DesignerApiDomains.DESIGNER_API,
480 String.format(BLUEPRINT_MODEL_ID_FAILURE_MSG, blueprintId))
481 } catch (e: BluePrintException) {
482 e.http(ErrorCatalogCodes.IO_FILE_INTERRUPT)
483 val errorMsg = "Error in Upload CBA."
484 throw e.updateErrorMessage(DesignerApiDomains.DESIGNER_API, errorMsg,
485 "Wrong enriched CBA.")
486 } catch (e: IOException) {
487 throw httpProcessorException(ErrorCatalogCodes.IO_FILE_INTERRUPT, DesignerApiDomains.DESIGNER_API,
488 "Error in Upload CBA: ${e.errorMessageOrDefault()}", e.errorCauseOrDefault())
490 // Clean blueprint script cache
491 val cacheKey = BluePrintFileUtils
492 .compileCacheKey(normalizedPathName(bluePrintLoadConfiguration.blueprintWorkingPath, saveId))
493 BluePrintCompileCache.cleanClassLoader(cacheKey)
494 deleteNBDir(blueprintArchive)
495 deleteNBDir(blueprintWorking)
499 /** Common CBA download function for RestController and GRPC Handler, the [fileSource] may be
500 * byteArray or File Part type.*/
501 open fun download(name: String, version: String): ByteArray {
503 val blueprintModel = getBlueprintModelByNameAndVersion(name, version)
504 return blueprintModel.blueprintModelContent?.content
505 ?: throw httpProcessorException(ErrorCatalogCodes.RESOURCE_NOT_FOUND, DesignerApiDomains.DESIGNER_API,
506 "Error while downloading the CBA file: couldn't get model content")
507 } catch (e: BluePrintException) {
508 e.http(ErrorCatalogCodes.RESOURCE_NOT_FOUND)
509 val errorMsg = "Fail to get Blueprint Model content."
510 throw e.updateErrorMessage(DesignerApiDomains.DESIGNER_API, errorMsg,
511 "Wrong name and version was provide.")
515 /** Common CBA Enrich function for RestController and GRPC Handler, the [fileSource] may be
516 * byteArray or File Part type.*/
517 open suspend fun enrichBlueprintFileSource(fileSource: Any): ByteArray {
518 val enhanceId = UUID.randomUUID().toString()
519 val blueprintArchive = normalizedPathName(bluePrintLoadConfiguration.blueprintArchivePath, enhanceId)
520 val blueprintWorkingDir = normalizedPathName(bluePrintLoadConfiguration.blueprintWorkingPath, enhanceId)
523 is FilePart -> BluePrintEnhancerUtils
524 .copyFilePartToEnhanceDir(fileSource, blueprintArchive, blueprintWorkingDir)
525 is ByteArray -> BluePrintEnhancerUtils
526 .copyByteArrayToEnhanceDir(fileSource, blueprintArchive, blueprintWorkingDir)
527 } // Enhance the Blue Prints
528 bluePrintEnhancerService.enhance(blueprintWorkingDir)
530 return BluePrintEnhancerUtils.compressEnhanceDirAndReturnByteArray(blueprintWorkingDir, blueprintArchive)
531 } catch (e: BluePrintException) {
532 e.http(ErrorCatalogCodes.IO_FILE_INTERRUPT)
533 val errorMsg = "Fail Enriching the CBA."
534 throw e.updateErrorMessage(DesignerApiDomains.DESIGNER_API, errorMsg)
535 } catch (e: IOException) {
536 throw httpProcessorException(ErrorCatalogCodes.IO_FILE_INTERRUPT, DesignerApiDomains.DESIGNER_API,
537 "Error while Enriching the CBA file.", e.errorCauseOrDefault())
539 BluePrintEnhancerUtils.cleanEnhancer(blueprintArchive, blueprintWorkingDir)
545 private const val BLUEPRINT_MODEL_ID_FAILURE_MSG = "failed to get blueprint model id(%s) from repo"
546 private const val BLUEPRINT_MODEL_NAME_VERSION_FAILURE_MSG = "failed to get blueprint model by name(%s)" + " and version(%s) from repo"