]> source.dussan.org Git - archiva.git/blob
1abe5a700e050063d0d94640798272aa0d3dd90b
[archiva.git] /
1 package org.apache.archiva.metadata.repository.storage.maven2;
2
3 /*
4  * Licensed to the Apache Software Foundation (ASF) under one
5  * or more contributor license agreements.  See the NOTICE file
6  * distributed with this work for additional information
7  * regarding copyright ownership.  The ASF licenses this file
8  * to you under the Apache License, Version 2.0 (the
9  * "License"); you may not use this file except in compliance
10  * with the License.  You may obtain a copy of the License at
11  *
12  *   http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17  * KIND, either express or implied.  See the License for the
18  * specific language governing permissions and limitations
19  * under the License.
20  */
21
22 import org.apache.archiva.checksum.ChecksumAlgorithm;
23 import org.apache.archiva.checksum.ChecksummedFile;
24 import org.apache.archiva.metadata.model.ArtifactMetadata;
25 import org.apache.archiva.metadata.model.ProjectMetadata;
26 import org.apache.archiva.metadata.model.ProjectVersionMetadata;
27 import org.apache.archiva.metadata.repository.MetadataRepository;
28 import org.apache.archiva.metadata.repository.MetadataRepositoryException;
29 import org.apache.archiva.metadata.repository.MetadataResolutionException;
30 import org.apache.archiva.metadata.repository.filter.Filter;
31 import org.apache.archiva.metadata.repository.storage.RepositoryPathTranslator;
32 import org.apache.archiva.metadata.repository.storage.RepositoryStorage;
33 import org.apache.archiva.reports.RepositoryProblemFacet;
34 import org.apache.maven.archiva.common.utils.VersionUtil;
35 import org.apache.maven.archiva.configuration.ArchivaConfiguration;
36 import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration;
37 import org.apache.maven.archiva.xml.XMLException;
38 import org.apache.maven.model.CiManagement;
39 import org.apache.maven.model.Dependency;
40 import org.apache.maven.model.IssueManagement;
41 import org.apache.maven.model.License;
42 import org.apache.maven.model.MailingList;
43 import org.apache.maven.model.Model;
44 import org.apache.maven.model.Organization;
45 import org.apache.maven.model.Scm;
46 import org.apache.maven.model.building.DefaultModelBuildingRequest;
47 import org.apache.maven.model.building.ModelBuilder;
48 import org.apache.maven.model.building.ModelBuildingException;
49 import org.apache.maven.model.building.ModelBuildingRequest;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import java.io.File;
54 import java.io.FilenameFilter;
55 import java.io.IOException;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collection;
59 import java.util.Collections;
60 import java.util.Date;
61 import java.util.List;
62
63 /**
64  * @plexus.component role="org.apache.archiva.metadata.repository.storage.RepositoryStorage" role-hint="maven2"
65  */
66 public class Maven2RepositoryStorage
67     implements RepositoryStorage
68 {
69     /**
70      * @plexus.requirement
71      */
72     private ModelBuilder builder;
73
74     /**
75      * @plexus.requirement
76      */
77     private ArchivaConfiguration archivaConfiguration;
78
79     /**
80      * @plexus.requirement role-hint="maven2"
81      */
82     private RepositoryPathTranslator pathTranslator;
83
84     /**
85      * @plexus.requirement
86      */
87     private MetadataRepository metadataRepository;
88
89     private final static Logger log = LoggerFactory.getLogger( Maven2RepositoryStorage.class );
90
91     private static final String METADATA_FILENAME = "maven-metadata.xml";
92
93     private static final String PROBLEM_MISSING_POM = "missing-pom";
94
95     private static final String PROBLEM_INVALID_POM = "invalid-pom";
96
97     private static final String PROBLEM_MISLOCATED_POM = "mislocated-pom";
98
99     private static final List<String> POTENTIAL_PROBLEMS = Arrays.asList( PROBLEM_INVALID_POM, PROBLEM_MISSING_POM,
100                                                                           PROBLEM_MISLOCATED_POM );
101
102     public ProjectMetadata readProjectMetadata( String repoId, String namespace, String projectId )
103     {
104         // TODO: could natively implement the "shared model" concept from the browse action to avoid needing it there?
105         return null;
106     }
107
108     public ProjectVersionMetadata readProjectVersionMetadata( String repoId, String namespace, String projectId,
109                                                               String projectVersion )
110         throws MetadataResolutionException
111     {
112         // Remove problems associated with this version, since we'll be re-adding any that still exist
113         // TODO: an event mechanism would remove coupling to the problem reporting plugin
114         // TODO: this removes all problems - do we need something that just removes the problems created by this resolver?
115         String name = RepositoryProblemFacet.createName( namespace, projectId, projectVersion, null );
116         try
117         {
118             metadataRepository.removeMetadataFacet( repoId, RepositoryProblemFacet.FACET_ID, name );
119         }
120         catch ( MetadataRepositoryException e )
121         {
122             log.warn( "Unable to remove repository problem facets for the version being removed: " + e.getMessage(),
123                       e );
124         }
125
126         ManagedRepositoryConfiguration repositoryConfiguration =
127             archivaConfiguration.getConfiguration().findManagedRepositoryById( repoId );
128
129         String artifactVersion = projectVersion;
130
131         File basedir = new File( repositoryConfiguration.getLocation() );
132         if ( VersionUtil.isSnapshot( projectVersion ) )
133         {
134             File metadataFile = pathTranslator.toFile( basedir, namespace, projectId, projectVersion,
135                                                        METADATA_FILENAME );
136             try
137             {
138                 MavenRepositoryMetadata metadata = MavenRepositoryMetadataReader.read( metadataFile );
139
140                 // re-adjust to timestamp if present, otherwise retain the original -SNAPSHOT filename
141                 MavenRepositoryMetadata.Snapshot snapshotVersion = metadata.getSnapshotVersion();
142                 if ( snapshotVersion != null )
143                 {
144                     artifactVersion = artifactVersion.substring( 0, artifactVersion.length() -
145                         8 ); // remove SNAPSHOT from end
146                     artifactVersion =
147                         artifactVersion + snapshotVersion.getTimestamp() + "-" + snapshotVersion.getBuildNumber();
148                 }
149             }
150             catch ( XMLException e )
151             {
152                 // unable to parse metadata - log it, and continue with the version as the original SNAPSHOT version
153                 log.warn( "Invalid metadata: " + metadataFile + " - " + e.getMessage() );
154             }
155         }
156
157         // TODO: won't work well with some other layouts, might need to convert artifact parts to ID by path translator
158         String id = projectId + "-" + artifactVersion + ".pom";
159         File file = pathTranslator.toFile( basedir, namespace, projectId, projectVersion, id );
160
161         if ( !file.exists() )
162         {
163             // TODO: an event mechanism would remove coupling to the problem reporting plugin
164             addProblemReport( repoId, namespace, projectId, projectVersion, PROBLEM_MISSING_POM,
165                               "The artifact's POM file '" + file + "' was missing" );
166
167             // metadata could not be resolved
168             return null;
169         }
170
171         ModelBuildingRequest req = new DefaultModelBuildingRequest();
172         req.setProcessPlugins( false );
173         req.setPomFile( file );
174         req.setModelResolver( new RepositoryModelResolver( basedir, pathTranslator ) );
175         req.setValidationLevel( ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL );
176
177         Model model;
178         try
179         {
180             model = builder.build( req ).getEffectiveModel();
181         }
182         catch ( ModelBuildingException e )
183         {
184             addProblemReport( repoId, namespace, projectId, projectVersion, PROBLEM_INVALID_POM,
185                               "The artifact's POM file '" + file + "' was invalid: " + e.getMessage() );
186
187             throw new MetadataResolutionException( e.getMessage() );
188         }
189
190         // Check if the POM is in the correct location
191         boolean correctGroupId = namespace.equals( model.getGroupId() );
192         boolean correctArtifactId = projectId.equals( model.getArtifactId() );
193         boolean correctVersion = projectVersion.equals( model.getVersion() );
194         if ( !correctGroupId || !correctArtifactId || !correctVersion )
195         {
196             StringBuilder message = new StringBuilder( "Incorrect POM coordinates in '" + file + "':" );
197             if ( !correctGroupId )
198             {
199                 message.append( "\nIncorrect group ID: " ).append( model.getGroupId() );
200             }
201             if ( !correctArtifactId )
202             {
203                 message.append( "\nIncorrect artifact ID: " ).append( model.getArtifactId() );
204             }
205             if ( !correctVersion )
206             {
207                 message.append( "\nIncorrect version: " ).append( model.getVersion() );
208             }
209
210             String msg = message.toString();
211             addProblemReport( repoId, namespace, projectId, projectVersion, PROBLEM_MISLOCATED_POM, msg );
212
213             throw new MetadataResolutionException( msg );
214         }
215
216         ProjectVersionMetadata metadata = new ProjectVersionMetadata();
217         metadata.setCiManagement( convertCiManagement( model.getCiManagement() ) );
218         metadata.setDescription( model.getDescription() );
219         metadata.setId( projectVersion );
220         metadata.setIssueManagement( convertIssueManagement( model.getIssueManagement() ) );
221         metadata.setLicenses( convertLicenses( model.getLicenses() ) );
222         metadata.setMailingLists( convertMailingLists( model.getMailingLists() ) );
223         metadata.setDependencies( convertDependencies( model.getDependencies() ) );
224         metadata.setName( model.getName() );
225         metadata.setOrganization( convertOrganization( model.getOrganization() ) );
226         metadata.setScm( convertScm( model.getScm() ) );
227         metadata.setUrl( model.getUrl() );
228
229         MavenProjectFacet facet = new MavenProjectFacet();
230         facet.setGroupId( model.getGroupId() != null ? model.getGroupId() : model.getParent().getGroupId() );
231         facet.setArtifactId( model.getArtifactId() );
232         facet.setPackaging( model.getPackaging() );
233         if ( model.getParent() != null )
234         {
235             MavenProjectParent parent = new MavenProjectParent();
236             parent.setGroupId( model.getParent().getGroupId() );
237             parent.setArtifactId( model.getParent().getArtifactId() );
238             parent.setVersion( model.getParent().getVersion() );
239             facet.setParent( parent );
240         }
241         metadata.addFacet( facet );
242
243         return metadata;
244     }
245
246     private void addProblemReport( String repoId, String namespace, String projectId, String projectVersion,
247                                    String problemId, String message )
248     {
249         // TODO: an event mechanism would remove coupling to the problem reporting plugin and allow other plugins to
250         //       generate metadata on the fly if appropriately checked for missing facets in the resolver
251         RepositoryProblemFacet problem = new RepositoryProblemFacet();
252         problem.setProblem( problemId );
253         problem.setMessage( message );
254         problem.setProject( projectId );
255         problem.setNamespace( namespace );
256         problem.setRepositoryId( repoId );
257         problem.setVersion( projectVersion );
258
259         try
260         {
261             metadataRepository.addMetadataFacet( repoId, problem );
262         }
263         catch ( MetadataRepositoryException e )
264         {
265             log.warn( "Unable to add repository problem facets for the version being removed: " + e.getMessage(), e );
266         }
267     }
268
269     private List<org.apache.archiva.metadata.model.Dependency> convertDependencies( List<Dependency> dependencies )
270     {
271         List<org.apache.archiva.metadata.model.Dependency> l =
272             new ArrayList<org.apache.archiva.metadata.model.Dependency>();
273         for ( Dependency dependency : dependencies )
274         {
275             org.apache.archiva.metadata.model.Dependency newDependency =
276                 new org.apache.archiva.metadata.model.Dependency();
277             newDependency.setArtifactId( dependency.getArtifactId() );
278             newDependency.setClassifier( dependency.getClassifier() );
279             newDependency.setGroupId( dependency.getGroupId() );
280             newDependency.setOptional( dependency.isOptional() );
281             newDependency.setScope( dependency.getScope() );
282             newDependency.setSystemPath( dependency.getSystemPath() );
283             newDependency.setType( dependency.getType() );
284             newDependency.setVersion( dependency.getVersion() );
285             l.add( newDependency );
286         }
287         return l;
288     }
289
290     private org.apache.archiva.metadata.model.Scm convertScm( Scm scm )
291     {
292         org.apache.archiva.metadata.model.Scm newScm = null;
293         if ( scm != null )
294         {
295             newScm = new org.apache.archiva.metadata.model.Scm();
296             newScm.setConnection( scm.getConnection() );
297             newScm.setDeveloperConnection( scm.getDeveloperConnection() );
298             newScm.setUrl( scm.getUrl() );
299         }
300         return newScm;
301     }
302
303     private org.apache.archiva.metadata.model.Organization convertOrganization( Organization organization )
304     {
305         org.apache.archiva.metadata.model.Organization org = null;
306         if ( organization != null )
307         {
308             org = new org.apache.archiva.metadata.model.Organization();
309             org.setName( organization.getName() );
310             org.setUrl( organization.getUrl() );
311         }
312         return org;
313     }
314
315     private List<org.apache.archiva.metadata.model.License> convertLicenses( List<License> licenses )
316     {
317         List<org.apache.archiva.metadata.model.License> l = new ArrayList<org.apache.archiva.metadata.model.License>();
318         for ( License license : licenses )
319         {
320             org.apache.archiva.metadata.model.License newLicense = new org.apache.archiva.metadata.model.License();
321             newLicense.setName( license.getName() );
322             newLicense.setUrl( license.getUrl() );
323             l.add( newLicense );
324         }
325         return l;
326     }
327
328     private List<org.apache.archiva.metadata.model.MailingList> convertMailingLists( List<MailingList> mailingLists )
329     {
330         List<org.apache.archiva.metadata.model.MailingList> l =
331             new ArrayList<org.apache.archiva.metadata.model.MailingList>();
332         for ( MailingList mailingList : mailingLists )
333         {
334             org.apache.archiva.metadata.model.MailingList newMailingList =
335                 new org.apache.archiva.metadata.model.MailingList();
336             newMailingList.setName( mailingList.getName() );
337             newMailingList.setMainArchiveUrl( mailingList.getArchive() );
338             newMailingList.setPostAddress( mailingList.getPost() );
339             newMailingList.setSubscribeAddress( mailingList.getSubscribe() );
340             newMailingList.setUnsubscribeAddress( mailingList.getUnsubscribe() );
341             newMailingList.setOtherArchives( mailingList.getOtherArchives() );
342             l.add( newMailingList );
343         }
344         return l;
345     }
346
347     private org.apache.archiva.metadata.model.IssueManagement convertIssueManagement( IssueManagement issueManagement )
348     {
349         org.apache.archiva.metadata.model.IssueManagement im = null;
350         if ( issueManagement != null )
351         {
352             im = new org.apache.archiva.metadata.model.IssueManagement();
353             im.setSystem( issueManagement.getSystem() );
354             im.setUrl( issueManagement.getUrl() );
355         }
356         return im;
357     }
358
359     private org.apache.archiva.metadata.model.CiManagement convertCiManagement( CiManagement ciManagement )
360     {
361         org.apache.archiva.metadata.model.CiManagement ci = null;
362         if ( ciManagement != null )
363         {
364             ci = new org.apache.archiva.metadata.model.CiManagement();
365             ci.setSystem( ciManagement.getSystem() );
366             ci.setUrl( ciManagement.getUrl() );
367         }
368         return ci;
369     }
370
371     public Collection<String> listRootNamespaces( String repoId, Filter<String> filter )
372     {
373         File dir = getRepositoryBasedir( repoId );
374
375         return getSortedFiles( dir, filter );
376     }
377
378     private static Collection<String> getSortedFiles( File dir, Filter<String> filter )
379     {
380         List<String> fileNames;
381         String[] files = dir.list( new DirectoryFilter( filter ) );
382         if ( files != null )
383         {
384             fileNames = new ArrayList<String>( Arrays.asList( files ) );
385             Collections.sort( fileNames );
386         }
387         else
388         {
389             fileNames = Collections.emptyList();
390         }
391         return fileNames;
392     }
393
394     private File getRepositoryBasedir( String repoId )
395     {
396         ManagedRepositoryConfiguration repositoryConfiguration =
397             archivaConfiguration.getConfiguration().findManagedRepositoryById( repoId );
398
399         return new File( repositoryConfiguration.getLocation() );
400     }
401
402     public Collection<String> listNamespaces( String repoId, String namespace, Filter<String> filter )
403     {
404         File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace );
405
406         // scan all the directories which are potential namespaces. Any directories known to be projects are excluded
407         List<String> namespaces = new ArrayList<String>();
408         File[] files = dir.listFiles( new DirectoryFilter( filter ) );
409         if ( files != null )
410         {
411             for ( File file : files )
412             {
413                 if ( !isProject( file, filter ) )
414                 {
415                     namespaces.add( file.getName() );
416                 }
417             }
418         }
419         Collections.sort( namespaces );
420         return namespaces;
421     }
422
423     public Collection<String> listProjects( String repoId, String namespace, Filter<String> filter )
424     {
425         File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace );
426
427         // scan all directories in the namespace, and only include those that are known to be projects
428         List<String> projects = new ArrayList<String>();
429         File[] files = dir.listFiles( new DirectoryFilter( filter ) );
430         if ( files != null )
431         {
432             for ( File file : files )
433             {
434                 if ( isProject( file, filter ) )
435                 {
436                     projects.add( file.getName() );
437                 }
438             }
439         }
440         Collections.sort( projects );
441         return projects;
442     }
443
444     public Collection<String> listProjectVersions( String repoId, String namespace, String projectId,
445                                                    Filter<String> filter )
446     {
447         File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace, projectId );
448
449         // all directories in a project directory can be considered a version
450         return getSortedFiles( dir, filter );
451     }
452
453     public Collection<ArtifactMetadata> readArtifactsMetadata( String repoId, String namespace, String projectId,
454                                                                String projectVersion, Filter<String> filter )
455     {
456         File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace, projectId, projectVersion );
457
458         // all files that are not metadata and not a checksum / signature are considered artifacts
459         File[] files = dir.listFiles( new ArtifactDirectoryFilter( filter ) );
460
461         List<ArtifactMetadata> artifacts = new ArrayList<ArtifactMetadata>();
462         if ( files != null )
463         {
464             for ( File file : files )
465             {
466                 ArtifactMetadata metadata = getArtifactFromFile( repoId, namespace, projectId, projectVersion, file );
467                 artifacts.add( metadata );
468             }
469         }
470         return artifacts;
471     }
472
473     public ArtifactMetadata readArtifactMetadataFromPath( String repoId, String path )
474     {
475         ArtifactMetadata metadata = pathTranslator.getArtifactForPath( repoId, path );
476
477         populateArtifactMetadataFromFile( metadata, new File( getRepositoryBasedir( repoId ), path ) );
478
479         return metadata;
480     }
481
482     private ArtifactMetadata getArtifactFromFile( String repoId, String namespace, String projectId,
483                                                   String projectVersion, File file )
484     {
485         ArtifactMetadata metadata = pathTranslator.getArtifactFromId( repoId, namespace, projectId, projectVersion,
486                                                                       file.getName() );
487
488         populateArtifactMetadataFromFile( metadata, file );
489
490         return metadata;
491     }
492
493     private static void populateArtifactMetadataFromFile( ArtifactMetadata metadata, File file )
494     {
495         metadata.setWhenGathered( new Date() );
496         metadata.setFileLastModified( file.lastModified() );
497         ChecksummedFile checksummedFile = new ChecksummedFile( file );
498         try
499         {
500             metadata.setMd5( checksummedFile.calculateChecksum( ChecksumAlgorithm.MD5 ) );
501         }
502         catch ( IOException e )
503         {
504             log.error( "Unable to checksum file " + file + ": " + e.getMessage() );
505         }
506         try
507         {
508             metadata.setSha1( checksummedFile.calculateChecksum( ChecksumAlgorithm.SHA1 ) );
509         }
510         catch ( IOException e )
511         {
512             log.error( "Unable to checksum file " + file + ": " + e.getMessage() );
513         }
514         metadata.setSize( file.length() );
515     }
516
517     private boolean isProject( File dir, Filter<String> filter )
518     {
519         // scan directories for a valid project version subdirectory, meaning this must be a project directory
520         File[] files = dir.listFiles( new DirectoryFilter( filter ) );
521         if ( files != null )
522         {
523             for ( File file : files )
524             {
525                 if ( isProjectVersion( file ) )
526                 {
527                     return true;
528                 }
529             }
530         }
531
532         // if a metadata file is present, check if this is the "artifactId" directory, marking it as a project
533         MavenRepositoryMetadata metadata = readMetadata( dir );
534         if ( metadata != null && dir.getName().equals( metadata.getArtifactId() ) )
535         {
536             return true;
537         }
538
539         return false;
540     }
541
542     private boolean isProjectVersion( File dir )
543     {
544         final String artifactId = dir.getParentFile().getName();
545         final String projectVersion = dir.getName();
546
547         // check if there is a POM artifact file to ensure it is a version directory
548         File[] files;
549         if ( VersionUtil.isSnapshot( projectVersion ) )
550         {
551             files = dir.listFiles( new FilenameFilter()
552             {
553                 public boolean accept( File dir, String name )
554                 {
555                     if ( name.startsWith( artifactId + "-" ) && name.endsWith( ".pom" ) )
556                     {
557                         String v = name.substring( artifactId.length() + 1, name.length() - 4 );
558                         v = VersionUtil.getBaseVersion( v );
559                         if ( v.equals( projectVersion ) )
560                         {
561                             return true;
562                         }
563                     }
564                     return false;
565                 }
566             } );
567         }
568         else
569         {
570             final String pomFile = artifactId + "-" + projectVersion + ".pom";
571             files = dir.listFiles( new FilenameFilter()
572             {
573                 public boolean accept( File dir, String name )
574                 {
575                     return pomFile.equals( name );
576                 }
577             } );
578         }
579         if ( files != null && files.length > 0 )
580         {
581             return true;
582         }
583
584         // if a metadata file is present, check if this is the "version" directory, marking it as a project version
585         MavenRepositoryMetadata metadata = readMetadata( dir );
586         if ( metadata != null && projectVersion.equals( metadata.getVersion() ) )
587         {
588             return true;
589         }
590
591         return false;
592     }
593
594     private MavenRepositoryMetadata readMetadata( File directory )
595     {
596         MavenRepositoryMetadata metadata = null;
597         File metadataFile = new File( directory, METADATA_FILENAME );
598         if ( metadataFile.exists() )
599         {
600             try
601             {
602                 metadata = MavenRepositoryMetadataReader.read( metadataFile );
603             }
604             catch ( XMLException e )
605             {
606                 // ignore missing or invalid metadata
607             }
608         }
609         return metadata;
610     }
611
612     private static class DirectoryFilter
613         implements FilenameFilter
614     {
615         private final Filter<String> filter;
616
617         public DirectoryFilter( Filter<String> filter )
618         {
619             this.filter = filter;
620         }
621
622         public boolean accept( File dir, String name )
623         {
624             if ( !filter.accept( name ) )
625             {
626                 return false;
627             }
628             else if ( name.startsWith( "." ) )
629             {
630                 return false;
631             }
632             else if ( !new File( dir, name ).isDirectory() )
633             {
634                 return false;
635             }
636             return true;
637         }
638     }
639
640     private class ArtifactDirectoryFilter
641         implements FilenameFilter
642     {
643         private final Filter<String> filter;
644
645         public ArtifactDirectoryFilter( Filter<String> filter )
646         {
647             this.filter = filter;
648         }
649
650         public boolean accept( File dir, String name )
651         {
652             // TODO compare to logic in maven-repository-layer
653             if ( !filter.accept( name ) )
654             {
655                 return false;
656             }
657             else if ( name.startsWith( "." ) )
658             {
659                 return false;
660             }
661             else if ( name.endsWith( ".md5" ) || name.endsWith( ".sha1" ) || name.endsWith( ".asc" ) )
662             {
663                 return false;
664             }
665             else if ( name.equals( METADATA_FILENAME ) )
666             {
667                 return false;
668             }
669             else if ( new File( dir, name ).isDirectory() )
670             {
671                 return false;
672             }
673             return true;
674         }
675     }
676 }