3bb383fdd27bc4c1afac39588880e60f63226a13
[vnfsdk/refrepo.git] /
1 /**
2  * Copyright 2017 Huawei Technologies Co., Ltd.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.onap.vnfsdk.marketplace.wrapper;
17
18 import java.io.BufferedInputStream;
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.concurrent.Callable;
26 import java.util.concurrent.ExecutorService;
27 import java.util.concurrent.Executors;
28
29 import javax.ws.rs.core.HttpHeaders;
30 import javax.ws.rs.core.MediaType;
31 import javax.ws.rs.core.Response;
32 import javax.ws.rs.core.Response.Status;
33
34 import net.sf.json.JSONObject;
35 import org.apache.commons.io.IOUtils;
36 import org.apache.commons.lang3.StringUtils;
37 import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
38 import org.onap.vnfsdk.marketplace.common.CommonConstant;
39 import org.onap.vnfsdk.marketplace.common.FileUtil;
40 import org.onap.vnfsdk.marketplace.common.JsonUtil;
41 import org.onap.vnfsdk.marketplace.common.RestUtil;
42 import org.onap.vnfsdk.marketplace.common.ToolUtil;
43 import org.onap.vnfsdk.marketplace.db.entity.PackageData;
44 import org.onap.vnfsdk.marketplace.db.exception.MarketplaceResourceException;
45 import org.onap.vnfsdk.marketplace.db.resource.PackageManager;
46 import org.onap.vnfsdk.marketplace.db.util.MarketplaceDbUtil;
47 import org.onap.vnfsdk.marketplace.entity.request.PackageBasicInfo;
48 import org.onap.vnfsdk.marketplace.entity.response.PackageMeta;
49 import org.onap.vnfsdk.marketplace.entity.response.UploadPackageResponse;
50 import org.onap.vnfsdk.marketplace.filemanage.FileManagerFactory;
51 import org.onap.vnfsdk.marketplace.onboarding.entity.OnBoardingOperResult;
52 import org.onap.vnfsdk.marketplace.onboarding.entity.OnBoardingResult;
53 import org.onap.vnfsdk.marketplace.onboarding.entity.OnBoardingSteps;
54 import org.onap.vnfsdk.marketplace.onboarding.entity.OnBoradingRequest;
55 import org.onap.vnfsdk.marketplace.onboarding.hooks.functiontest.FunctionTestExceutor;
56 import org.onap.vnfsdk.marketplace.onboarding.hooks.functiontest.FunctionTestHook;
57 import org.onap.vnfsdk.marketplace.onboarding.hooks.validatelifecycle.LifecycleTestExceutor;
58 import org.onap.vnfsdk.marketplace.onboarding.hooks.validatelifecycle.ValidateLifecycleTestResponse;
59 import org.onap.vnfsdk.marketplace.onboarding.onboardmanager.OnBoardingHandler;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 public class PackageWrapper {
64     private static PackageWrapper packageWrapper;
65     private static final Logger LOG = LoggerFactory.getLogger(PackageWrapper.class);
66
67     /**
68      * get PackageWrapper instance.
69      * @return package wrapper instance
70      */
71     public static PackageWrapper getInstance() {
72         if (packageWrapper == null) {
73             packageWrapper = new PackageWrapper();
74         }
75         return packageWrapper;
76     }
77
78     public Response updateValidateStatus(InputStream inputStream, HttpHeaders head) throws Exception
79     {
80         String reqParam = IOUtils.toString(inputStream);
81         LOG.info("updateValidateStatus request param:"+reqParam);
82         if(StringUtils.isBlank(reqParam)) {
83             LOG.error("The updateValidateStatus request params can't be null");
84             return Response.status(Status.EXPECTATION_FAILED).build();
85         }
86         
87         ValidateLifecycleTestResponse lyfValidateResp = JsonUtil.fromJson(reqParam, ValidateLifecycleTestResponse.class);
88         if(!checkOperationSucess(lyfValidateResp))
89         {
90             return Response.status(Status.EXPECTATION_FAILED).build();
91         }
92         
93         String funcTestResponse = FunctionTestExceutor.executeFunctionTest(reqParam);
94         if(null == funcTestResponse)
95         {
96             return Response.status(Status.EXPECTATION_FAILED).build();
97         }
98         
99         JSONObject funcTestRspObject = JSONObject.fromObject(funcTestResponse);   
100         if(!funcTestRspObject.get("status").equals(CommonConstant.SUCCESS_STR))
101         {
102             return Response.status(Status.EXPECTATION_FAILED).build();
103         }
104                 
105         JSONObject result = new JSONObject(); 
106         result.put("msg","SUCCESS");
107         return Response.ok(ToolUtil.objectToString(result), MediaType.APPLICATION_JSON).build();
108     }
109     
110     private boolean checkOperationSucess(ValidateLifecycleTestResponse lyfValidateResp) 
111     {
112         boolean bOperStatus = false;
113         if(null == lyfValidateResp) 
114         {
115             LOG.error("ValidateLifecycleTestResponse  is NUll !!!");
116             return bOperStatus;
117         }
118         if(lyfValidateResp.getLifecycle_status().equalsIgnoreCase(CommonConstant.SUCCESS_STR) 
119                 && lyfValidateResp.getValidate_status().equalsIgnoreCase(CommonConstant.SUCCESS_STR))
120         {
121             LOG.error("Lifecycle/Validation Response failed :" + lyfValidateResp.getLifecycle_status() + File.separator + lyfValidateResp.getValidate_status());
122             bOperStatus =  true;
123         }
124         return bOperStatus;
125     }
126
127     /**
128      * query package list by condition.
129      * @param name package name
130      * @param provider package provider
131      * @param version package version
132      * @param deletionPending package deletionPending
133      * @param type package type
134      * @return Response
135      */
136     public Response queryPackageListByCond(String name, String provider, String version,
137             String deletionPending, String type) {
138         ArrayList<PackageData> dbresult = new ArrayList<PackageData>();
139         ArrayList<PackageMeta> result = new ArrayList<PackageMeta>();
140         LOG.info("query package info.name:" + name + " provider:" + provider + " version" + version
141                 + " deletionPending" + deletionPending + " type:" + type);
142         try {
143             dbresult =
144                     PackageManager.getInstance().queryPackage(name, provider, version, deletionPending, type);
145             result = PackageWrapperUtil.packageDataList2PackageMetaList(dbresult);
146             return Response.ok(ToolUtil.objectToString(result)).build();
147         } catch (MarketplaceResourceException e1) {
148             LOG.error("query package by csarId from db error ! " + e1.getMessage());
149             return RestUtil.getRestException(e1.getMessage());
150         }
151     }
152
153     /**
154      * query package by id.
155      * @param csarId package id
156      * @return Response
157      */
158     public Response queryPackageById(String csarId) {
159         PackageData dbResult = new PackageData();
160         PackageMeta result = new PackageMeta();
161         dbResult = PackageWrapperUtil.getPackageInfoById(csarId);
162         result = PackageWrapperUtil.packageData2PackageMeta(dbResult);
163         return Response.ok(ToolUtil.objectToString(result)).build();
164     }
165
166     /**
167      * upload package.
168      * @param uploadedInputStream inputStream
169      * @param fileDetail package detail
170      * @param head http header
171      * @return Response
172      * @throws Exception e
173      */
174     public Response uploadPackage(InputStream uploadedInputStream,
175             FormDataContentDisposition fileDetail, String details, HttpHeaders head) throws Exception 
176     {
177         LOG.info("Upload/Reupload request Received !!!!");
178         
179         String packageId = MarketplaceDbUtil.generateId();
180         return handlePackageUpload(packageId,uploadedInputStream, fileDetail, details, head);
181     }
182
183     /**
184      * Interface for Uploading package
185      * @param packageId
186      * @param uploadedInputStream
187      * @param fileDetail
188      * @param details
189      * @param head
190      * @return
191      * @throws IOException
192      * @throws MarketplaceResourceException
193      */
194     private Response handlePackageUpload(String packageId,InputStream uploadedInputStream, FormDataContentDisposition fileDetail,
195             String details, HttpHeaders head) throws IOException, MarketplaceResourceException 
196     {     
197         boolean bResult = handleDataValidate(packageId,uploadedInputStream,fileDetail);
198         if(!bResult)
199         {
200             LOG.error("Validation of Input received for Package Upload failed !!!");
201             return Response.status(Status.EXPECTATION_FAILED).build();
202         }
203
204         String fileName = "temp_"+ packageId + ".csar";
205         if (null != fileDetail)
206         {
207             LOG.info("the fileDetail = " + ToolUtil.objectToString(fileDetail));
208
209             fileName = ToolUtil.processFileName(fileDetail.getFileName());
210         }
211         
212         String localDirName = ToolUtil.getTempDir(CommonConstant.CATALOG_CSAR_DIR_NAME, fileName);
213
214         String contentRange = null;
215         if (head != null) 
216         {
217             contentRange = head.getHeaderString(CommonConstant.HTTP_HEADER_CONTENT_RANGE);
218         }
219         LOG.info("store package chunk file, fileName:" + fileName + ",contentRange:" + contentRange);
220         if (ToolUtil.isEmptyString(contentRange))
221         {
222             int fileSize = uploadedInputStream.available();
223             contentRange = "0-" + fileSize + "/" + fileSize;
224         }
225
226         String fileLocation = ToolUtil.storeChunkFileInLocal(localDirName, fileName, uploadedInputStream);
227         LOG.info("the fileLocation when upload package is :" + fileLocation);
228
229         uploadedInputStream.close();
230
231         PackageBasicInfo basicInfo = PackageWrapperUtil.getPacageBasicInfo(fileLocation);          
232         if (null == basicInfo.getType() || null == basicInfo.getProvider() || null == basicInfo.getVersion()) 
233         {
234             LOG.error("Package basicInfo is incorrect ! basicIonfo = " + ToolUtil.objectToString(basicInfo));
235             return Response.serverError().build();
236         }
237
238         UploadPackageResponse result = new UploadPackageResponse();      
239         Boolean isEnd = PackageWrapperUtil.isUploadEnd(contentRange, fileName);
240         if (isEnd) 
241         {
242             PackageMeta packageMeta = PackageWrapperUtil.getPackageMeta(packageId,fileName, fileLocation, basicInfo, details);
243             
244             String path =  basicInfo.getType().toString() + File.separator + basicInfo.getProvider() + File.separator +  packageMeta.getCsarId() + File.separator + fileName.replace(".csar", "") + File.separator + basicInfo.getVersion();
245             String dowloadUri = File.separator + path + File.separator;
246             packageMeta.setDownloadUri(dowloadUri);
247             
248             LOG.info("dest path is : " + path);
249             LOG.info("packageMeta = " + ToolUtil.objectToString(packageMeta));
250
251             PackageData packageData = PackageWrapperUtil.getPackageData(packageMeta);
252             
253             String destPath = File.separator + path + File.separator + File.separator;
254             boolean uploadResult = FileManagerFactory.createFileManager().upload(localDirName, destPath);
255             if (uploadResult) 
256             {
257                 //Create OnBoarding Request 
258                 //--------------------------
259                 OnBoradingRequest oOnboradingRequest = new OnBoradingRequest();
260                 oOnboradingRequest.setCsarId(packageId);
261                 oOnboradingRequest.setPackageName(fileName);
262                 oOnboradingRequest.setPackagePath(localDirName);
263                 
264                 //Upload the Package to CATALOUGE and get CSARID
265                 //---------------------------------------------
266                 //String catalougeCsarId = LifecycleTestExceutor.uploadPackageToCatalouge(oOnboradingRequest);
267                 //if((null == catalougeCsarId) || catalougeCsarId.isEmpty())
268                 //{
269                 //    LOG.error("Failed to Upload Package to catalougeCsarId " + ToolUtil.objectToString(basicInfo));
270                     //return Response.status(Status.INTERNAL_SERVER_ERROR).build();
271                 // }
272                 //oOnboradingRequest.setCsarIdCatalouge(catalougeCsarId);
273                 //LOG.info("catalougeCsarId :" + catalougeCsarId);
274
275                 
276                 //Update Default download count to -1
277                 packageData.setCsarId(packageId);
278                 packageData.setDownloadCount(-1);
279                 PackageData packateDbData = PackageManager.getInstance().addPackage(packageData);
280
281                 LOG.info("Store package data to database succed ! packateDbData = "  + ToolUtil.objectToString(packateDbData));
282                 LOG.info("upload package file end, fileName:" + fileName);
283
284                 result.setCsarId(packateDbData.getCsarId());
285
286                 //Assign  OnBoarding Request to OnBoarding Handler
287                 //------------------------------------------------
288                 addOnBoardingRequest(oOnboradingRequest);
289
290                 LOG.info("OnboradingRequest Data : "  + ToolUtil.objectToString(oOnboradingRequest));
291             }
292         }
293         return Response.ok(ToolUtil.objectToString(result), MediaType.APPLICATION_JSON).build();
294     }
295
296     /**
297      * Execute OnBarding request
298      * @param oOnboradingRequest
299      */
300     private void addOnBoardingRequest(final OnBoradingRequest oOnboradingRequest) 
301     {
302         ExecutorService es = Executors.newFixedThreadPool(CommonConstant.ONBOARDING_THREAD_COUNT);
303         es.submit(new Callable<Integer>()
304         {
305             public Integer call() throws Exception 
306             {
307                 new OnBoardingHandler().handleOnBoardingReq(oOnboradingRequest);
308                 return CommonConstant.SUCESS;
309             }
310         });
311     }
312
313     /**
314      * delete package by package id.
315      * @param csarId package id
316      * @return Response
317      */
318     public Response delPackage(String csarId) {
319         LOG.info("delete package  info.csarId:" + csarId);
320         if (ToolUtil.isEmptyString(csarId)) {
321             LOG.error("delete package  fail, csarid is null");
322             return Response.serverError().build();
323         }
324         deletePackageDataById(csarId);
325         return Response.ok().build();
326     }
327
328     /**
329      * Delete Package by CSAR ID
330      * @param csarId
331      */
332     private void  deletePackageDataById(String csarId) {
333         String packagePath = PackageWrapperUtil.getPackagePath(csarId);
334         if (packagePath == null) {
335             LOG.error("package path is null! ");
336         }
337         
338         //Delete Package
339         FileManagerFactory.createFileManager().delete(packagePath);
340         //Delete Results Data
341         FileManagerFactory.createFileManager().delete(File.separator + csarId);
342
343         
344         //delete package data from database
345         try {
346             PackageManager.getInstance().deletePackage(csarId);
347         } catch (MarketplaceResourceException e1) {
348             LOG.error("delete package  by csarId from db error ! " + e1.getMessage(), e1);
349         }
350     }
351
352     /**
353      * download package by package id.
354      * @param csarId package id
355      * @return Response
356      */
357     public Response downloadCsarPackagesById(String csarId) {
358         PackageData packageData = PackageWrapperUtil.getPackageInfoById(csarId);
359         
360         String packageName = packageData.getName();
361         String path = org.onap.vnfsdk.marketplace.filemanage.http.ToolUtil.getHttpServerAbsolutePath() +File.separatorChar+packageData.getType()+File.separatorChar+
362                 packageData.getProvider()+File.separatorChar+ packageData.getCsarId() +File.separator +packageName+File.separatorChar+packageData.getVersion() 
363                 +File.separator + packageName + ".csar";
364         
365         LOG.info("downloadCsarPackagesById path is :  " + path);
366         
367         File csarFile = new File(path);
368         if (!csarFile.exists()) {
369             return Response.status(Status.INTERNAL_SERVER_ERROR).build();
370         }
371
372         LOG.info("downloadCsarPackagesById ABS path is :  " + csarFile.getAbsolutePath());
373         
374         try 
375         {
376             InputStream fis = new BufferedInputStream(new FileInputStream(csarFile.getAbsolutePath()));
377             return Response.ok(fis)
378                     .header("Content-Disposition", "attachment; filename=\"" + csarFile.getName() + "\"")
379                     .build();
380         } 
381         catch (Exception e1) 
382         {
383             LOG.error("download vnf package fail.", e1);
384             return RestUtil.getRestException(e1.getMessage());
385         }
386     }
387
388     /**
389      * get package file uri.
390      * @param csarId package id
391      * @param relativePath file relative path
392      * @return Response
393      */
394     public Response getCsarFileUri(String csarId) {
395         return downloadCsarPackagesById(csarId);
396     }
397
398     /**
399      * Interface to Update Download count for CSAR ID
400      * @param csarId
401      * @return
402      */
403     public Response updateDwonloadCount(String csarId) {
404         return handleDownladCountUpdate(csarId) ?  
405                 Response.ok().build() : 
406                     Response.status(Status.EXPECTATION_FAILED).build();
407     }
408
409     /**
410      * Handle downlowa count update
411      * @param csarId
412      * @return
413      */
414     private boolean handleDownladCountUpdate(String csarId) {
415         boolean bupdateSucess = false;
416         try 
417         {
418             PackageManager.getInstance().updateDwonloadCount(csarId);
419             bupdateSucess = true;
420         } 
421         catch (Exception exp) 
422         {
423             LOG.error("Updating Donwload count failed for Package with ID !!! : " + exp.getMessage(), exp);
424         }
425         return bupdateSucess;
426     }
427
428     /**
429      * Interface to Re upload Package 
430      * @param csarId
431      * @param uploadedInputStream
432      * @param fileDetail
433      * @param details
434      * @param head
435      * @return
436      * @throws Exception
437      */
438     public Response reUploadPackage(String csarId,
439             InputStream uploadedInputStream, 
440             FormDataContentDisposition fileDetail,
441             String details, 
442             HttpHeaders head) throws Exception
443     {
444         LOG.info("Reupload request Received !!!!");
445         
446         //STEP 1: Validate Input Data  
447         //----------------------------
448         boolean bResult = handleDataValidate(csarId,uploadedInputStream,fileDetail);
449         if(!bResult)
450         {
451             LOG.error("Validation of Input received for Package Upload failed during Reload!!!");
452             return Response.status(Status.EXPECTATION_FAILED).build();
453         }
454
455         //STEP 2: Delete All Package Data based on package id
456         //----------------------------------------------------
457         deletePackageDataById(csarId);
458
459         //STEP 3: upload package with same package id
460         //-------------------------------------------
461         return handlePackageUpload(csarId,uploadedInputStream, fileDetail, details, head);
462     }
463
464     /**
465      * Interface to get OnBoarding Result by Operation Type
466      * @param csarId
467      * @param operTypeId
468      * @param operId
469      * @return
470      */
471     public Response getOnBoardingResult(String csarId, String operTypeId, String operId)
472     {
473         LOG.info("getOnBoardingResult request : csarId:" + csarId + " operTypeId:" + operTypeId + " operId:" + operId);
474         if ((null == csarId) || (null == operTypeId) || (null == operId)) {
475             return Response.status(Response.Status.BAD_REQUEST).build();
476         }
477         if ((csarId.isEmpty()) || (operTypeId.isEmpty()) || (operId.isEmpty())) {
478             return Response.status(Response.Status.BAD_REQUEST).build();
479         }
480         PackageData packageData = PackageWrapperUtil.getPackageInfoById(csarId);
481         if (null == packageData) {
482             return Response.status(Response.Status.PRECONDITION_FAILED).build();
483         }
484         
485         handleDelayExec(operId);
486         
487         OnBoardingResult oOnBoardingResult = FunctionTestHook.getOnBoardingResult(packageData);
488         if (null == oOnBoardingResult) {
489             return Response.status(Response.Status.PRECONDITION_FAILED).build();
490         }
491         filterOnBoardingResultByOperId(oOnBoardingResult, operId);
492
493         String strResult = ToolUtil.objectToString(oOnBoardingResult);
494         LOG.info("getOnBoardingResult response : " + strResult);
495         return Response.ok(strResult, "application/json").build();
496     }
497
498     
499     private void filterOnBoardingResultByOperId(OnBoardingResult oOnBoardingResult, String operId)
500     {
501         if (0 == operId.compareToIgnoreCase("all")) {
502             return;
503         }
504         if (0 == operId.compareToIgnoreCase("download"))
505         {
506             List<OnBoardingOperResult> operResultListTemp = new ArrayList<OnBoardingOperResult>();
507             OnBoardingOperResult operResultListTmp = new OnBoardingOperResult();
508             operResultListTmp.setOperId("download");
509             operResultListTmp.setStatus(0);
510             operResultListTemp.add(operResultListTmp);
511             oOnBoardingResult.setOperResult(operResultListTemp);
512             return;
513         }
514         List<OnBoardingOperResult> operResultListOut = new ArrayList<OnBoardingOperResult>();
515         List<OnBoardingOperResult> operResultList = oOnBoardingResult.getOperResult();
516         for (OnBoardingOperResult operResult : operResultList) {
517             if (0 == operResult.getOperId().compareToIgnoreCase(operId)) {
518                 operResultListOut.add(operResult);
519             }
520         }
521         oOnBoardingResult.setOperResult(operResultListOut);
522     }
523
524     /**
525      * Interface to get OnBoarding Status by Operation ID
526      * @param csarId
527      * @param operTypeId
528      * @return
529      */
530     public Response getOperResultByOperTypeId(String csarId, String operTypeId) 
531     {
532         LOG.error("getOnBoardingResult request : csarId:"+ csarId + " operTypeId:"+operTypeId);    
533         if(null == csarId || null == operTypeId || csarId.isEmpty()  || operTypeId.isEmpty())
534         {
535             return Response.status(Status.BAD_REQUEST).build(); 
536         }
537
538         PackageData packageData = PackageWrapperUtil.getPackageInfoById(csarId);
539         if(null == packageData)
540         {
541             LOG.error("Failed to find package for PackageID:"+ csarId); 
542             return Response.status(Status.PRECONDITION_FAILED).build(); 
543         } 
544
545         //Get result key to fetch Function Test Results
546         //---------------------------------------------
547         String strResult = FunctionTestHook.getFuncTestResults(packageData);
548         if(null == strResult)
549         {
550             LOG.error("NULL reponse for getOperResultByOperTypeId response :"+ strResult); 
551             return Response.status(Status.INTERNAL_SERVER_ERROR).build(); 
552         }   
553         LOG.info("getOperResultByOperTypeId response :"+ strResult);    
554         return Response.ok(strResult, MediaType.APPLICATION_JSON).build();
555     }
556
557     private boolean handleDataValidate(String packageId,InputStream uploadedInputStream, FormDataContentDisposition fileDetail) 
558     {
559         boolean bvalidateOk = false;
560         if ((null != uploadedInputStream) && (fileDetail != null) && !ToolUtil.isEmptyString(packageId)) 
561         {
562             bvalidateOk = true;
563         }
564         return bvalidateOk;
565     }
566
567     /**
568      * Interface to get OnBoarding Steps
569      * @return
570      */
571     public Response getOnBoardingSteps()
572     {
573         LOG.info("Get OnBoarding Steps request Received !!!");
574
575         String filePath = org.onap.vnfsdk.marketplace.filemanage.http.ToolUtil.getAppDeployPath() + File.separator +"generalconfig/OnBoardingSteps.json";            
576         LOG.info("Onboarding Steps Json file Path  :" + filePath);
577
578         OnBoardingSteps oOnBoardingSteps = (OnBoardingSteps)FileUtil.readJsonDatafFromFile(filePath, OnBoardingSteps.class);
579         if (null == oOnBoardingSteps) {
580             return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
581         }
582         String strResult = ToolUtil.objectToString(oOnBoardingSteps);
583         LOG.info("getOnBoardingSteps response :" + strResult);
584         return Response.ok(strResult, MediaType.APPLICATION_JSON).build();
585     }
586     
587     private void handleDelayExec(String operId) 
588     {
589         if (0 == operId.compareToIgnoreCase(CommonConstant.functionTest.FUNCTEST_EXEC)) 
590         {
591             try 
592             {
593                 Thread.sleep(8000);
594             } 
595             catch (InterruptedException e) 
596             {
597                 LOG.info("handleDelayExex response : " + e.getMessage());
598             }
599         }
600     }
601 }