001/*
002 * Copyright 2007 The Kuali Foundation
003 * 
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 * http://www.opensource.org/licenses/ecl2.php
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kualigan.maven.plugins.api;
017
018import java.io.DataInputStream;
019import java.io.File;
020import java.io.FileOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Properties;
028import java.util.StringTokenizer;
029
030import org.apache.commons.cli.CommandLine;
031import org.apache.commons.cli.Options;
032import org.apache.commons.cli.PosixParser;
033import org.apache.commons.exec.DefaultExecutor;
034import org.apache.commons.exec.ExecuteException;
035import org.apache.commons.exec.Executor;
036import org.apache.commons.exec.PumpStreamHandler;
037import org.apache.maven.archetype.Archetype;
038import org.apache.maven.artifact.repository.ArtifactRepositoryFactory;
039import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
040import org.apache.maven.plugin.AbstractMojo;
041import org.apache.maven.plugin.MojoExecutionException;
042import org.apache.maven.shared.invoker.DefaultInvocationRequest;
043import org.apache.maven.shared.invoker.DefaultInvoker;
044import org.apache.maven.shared.invoker.InvocationRequest;
045import org.apache.maven.shared.invoker.InvocationResult;
046import org.apache.maven.shared.invoker.Invoker;
047import org.apache.maven.shared.invoker.MavenInvocationException;
048import org.codehaus.plexus.archiver.Archiver;
049import org.codehaus.plexus.archiver.ArchiverException;
050import org.codehaus.plexus.archiver.UnArchiver;
051import org.codehaus.plexus.archiver.manager.ArchiverManager;
052import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
053import org.codehaus.plexus.archiver.util.DefaultFileSet;
054import org.codehaus.plexus.archiver.zip.ZipArchiver;
055import org.codehaus.plexus.component.annotations.Component;
056import org.codehaus.plexus.component.annotations.Requirement;
057import org.codehaus.plexus.components.interactivity.Prompter;
058import org.codehaus.plexus.components.io.fileselectors.IncludeExcludeFileSelector;
059import org.codehaus.plexus.util.FileUtils;
060import org.codehaus.plexus.util.StringUtils;
061import org.codehaus.plexus.util.cli.CommandLineUtils;
062
063/**
064 * Creates a prototype from the given KFS project resource. A KFS project resource can be either
065 * of the following:
066 * <ul>
067 *   <li>KFS war file</li>
068 *   <li>KFS project directory with source</li>
069 *   <li>KFS svn repo</li>
070 * </ul>
071 * 
072 * @author Leo Przybylski
073 */
074@Component(role = org.kualigan.maven.plugins.api.PrototypeHelper.class, hint="default")
075public class DefaultPrototypeHelper implements PrototypeHelper {
076    public static final String ROLE_HINT = "default";
077    
078    private static final Options OPTIONS = new Options();
079
080    private static final char SET_SYSTEM_PROPERTY = 'D';
081
082    private static final char OFFLINE = 'o';
083
084    private static final char REACTOR = 'r';
085
086    private static final char QUIET = 'q';
087
088    private static final char DEBUG = 'X';
089
090    private static final char ERRORS = 'e';
091
092    private static final char NON_RECURSIVE = 'N';
093
094    private static final char UPDATE_SNAPSHOTS = 'U';
095
096    private static final char ACTIVATE_PROFILES = 'P';
097
098    private static final String FORCE_PLUGIN_UPDATES = "cpu";
099
100    private static final String FORCE_PLUGIN_UPDATES2 = "up";
101
102    private static final String SUPPRESS_PLUGIN_UPDATES = "npu";
103
104    private static final String SUPPRESS_PLUGIN_REGISTRY = "npr";
105
106    private static final char CHECKSUM_FAILURE_POLICY = 'C';
107
108    private static final char CHECKSUM_WARNING_POLICY = 'c';
109
110    private static final char ALTERNATE_USER_SETTINGS = 's';
111
112    private static final String FAIL_FAST = "ff";
113
114    private static final String FAIL_AT_END = "fae";
115
116    private static final String FAIL_NEVER = "fn";
117    
118    private static final String ALTERNATE_POM_FILE = "f";
119    
120    /**
121     */
122    @Requirement
123    protected Archetype archetype;
124
125    /**
126     */
127    @Requirement
128    protected Prompter prompter;
129
130    /**
131     */
132    @Requirement
133    protected ArtifactRepositoryFactory artifactRepositoryFactory;
134
135    /**
136     */
137    @Requirement(role=org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout.class, hint="default")
138    protected ArtifactRepositoryLayout defaultArtifactRepositoryLayout;
139
140    @Requirement
141    protected ArchiverManager archiverManager;
142    
143    private AbstractMojo caller;
144
145    protected void logUnpack(final File file, final File location, final String includes, final String excludes ) {
146        if (!getCaller().getLog().isInfoEnabled()) {
147            return;
148        }
149
150        StringBuffer msg = new StringBuffer();
151        msg.append( "Unpacking " );
152        msg.append( file );
153        msg.append( " to " );
154        msg.append( location );
155
156        if ( includes != null && excludes != null ) {
157            msg.append( " with includes \"" );
158            msg.append( includes );
159            msg.append( "\" and excludes \"" );
160            msg.append( excludes );
161            msg.append( "\"" );
162        }
163        else if (includes != null) {
164            msg.append( " with includes \"" );
165            msg.append( includes );
166            msg.append( "\"" );
167        }
168        else if (excludes != null) {
169            msg.append( " with excludes \"" );
170            msg.append( excludes );
171            msg.append( "\"" );
172        }
173
174        getCaller().getLog().info(msg.toString());
175    }
176
177   
178    protected int executeCommandLine(final String ... args) throws ExecuteException, IOException {
179        final Executor exec = new DefaultExecutor();
180        final Map enviro = new HashMap();
181        try {
182            final Properties systemEnvVars = CommandLineUtils.getSystemEnvVars();
183            enviro.putAll(systemEnvVars);
184        } catch (IOException x) {
185            getCaller().getLog().error("Could not assign default system enviroment variables.", x);
186        }
187
188        final File workingDirectory = new File(System.getProperty("java.io.tmpdir"));
189        final org.apache.commons.exec.CommandLine commandLine = new org.apache.commons.exec.CommandLine(args[0]);
190        for (int i = 1; i < args.length; i++) {
191            commandLine.addArgument(args[i]);
192        }
193
194        exec.setStreamHandler(new PumpStreamHandler(System.out, System.err, System.in));
195        exec.setWorkingDirectory(workingDirectory);
196        return exec.execute(commandLine, enviro);
197    }
198
199    /**
200     * Handles repacking of the war as a jar file with classes, etc... Basically makes a jar of everything
201     * in the war file's WEB-INF/classes path.
202     * 
203     * @param file is the war file to repackage
204     * @return {@link File} instance of the repacked jar
205     */
206    public File repack(final File file, final String artifactId) throws MojoExecutionException {
207        final File workingDirectory = new File(System.getProperty("java.io.tmpdir") + File.separator
208                                               + artifactId + "-repack");
209        final File warDirectory     = new File(workingDirectory, "war");
210        final File repackDirectory  = new File(workingDirectory, artifactId);
211        final File classesDirectory = new File(warDirectory, "WEB-INF/classes");
212        final File retval           = new File(workingDirectory, artifactId + ".jar");
213        
214        try {
215            workingDirectory.mkdirs();
216            workingDirectory.mkdir();
217            workingDirectory.deleteOnExit();
218            warDirectory.mkdir();
219        }
220        catch (Exception e) {
221            throw new MojoExecutionException("Unable to create working directory for repackaging", e);
222        }
223        
224        unpack(file, warDirectory, "**/classes/**", null);
225        
226        try {
227            FileUtils.copyDirectoryStructure(classesDirectory, repackDirectory);
228        }
229        catch (Exception e) {
230            throw new MojoExecutionException("Unable to copy files into the repack directory");
231        }
232
233        try {
234            pack(retval, repackDirectory, "**/**", null);
235        }
236        catch (Exception e) {
237            throw new MojoExecutionException("Was unable to create the jar", e);
238        }
239        
240        return retval;
241    }
242    
243    /**
244     * Unpacks the archive file.
245     *
246     * @param file     File to be unpacked.
247     * @param location Location where to put the unpacked files.
248     * @param includes Comma separated list of file patterns to include i.e. <code>**&#47;.xml,
249     *                 **&#47;*.properties</code>
250     * @param excludes Comma separated list of file patterns to exclude i.e. <code>**&#47;*.xml,
251     *                 **&#47;*.properties</code>
252     */
253    protected void unpack(final File file, final File location, final String includes, final String excludes) throws MojoExecutionException {
254        try {
255            logUnpack(file, location, includes, excludes);
256
257            location.mkdirs();
258
259            final UnArchiver unArchiver;
260            unArchiver = archiverManager.getUnArchiver(file);
261            unArchiver.setSourceFile(file);
262            unArchiver.setDestDirectory(location);
263
264            if (StringUtils.isNotEmpty(excludes) || StringUtils.isNotEmpty(includes)) {
265                final IncludeExcludeFileSelector[] selectors =
266                    new IncludeExcludeFileSelector[]{ new IncludeExcludeFileSelector() };
267
268                if (StringUtils.isNotEmpty( excludes )) {
269                    selectors[0].setExcludes(excludes.split( "," ));
270                }
271
272                if (StringUtils.isNotEmpty( includes )) {
273                    selectors[0].setIncludes(includes.split( "," ));
274                }
275
276                unArchiver.setFileSelectors(selectors);
277            }
278
279            unArchiver.extract();
280        }
281        catch ( NoSuchArchiverException e ) {
282            throw new MojoExecutionException("Unknown archiver type", e);
283        }
284        catch (ArchiverException e) {
285            e.printStackTrace();
286            throw new MojoExecutionException(
287                "Error unpacking file: " + file + " to: " + location + "\r\n" + e.toString(), e );
288        }
289    }
290
291    /**
292     * Packs a jar
293     *
294     * @param file     Destination file.
295     * @param location Directory source.
296     * @param includes Comma separated list of file patterns to include i.e. <code>**&#47;.xml,
297     *                 **&#47;*.properties</code>
298     * @param excludes Comma separated list of file patterns to exclude i.e. <code>**&#47;*.xml,
299     *                 **&#47;*.properties</code>
300     */
301    protected void pack(final File file, final File location, final String includes, final String excludes) throws MojoExecutionException {
302        try {
303
304            final Archiver archiver;
305            archiver = archiverManager.getArchiver(file);
306            archiver.addFileSet(new DefaultFileSet() {{
307                setDirectory(location);
308                if (includes != null) {
309                    setIncludes(includes.split(","));
310                }
311                if (excludes != null) {
312                    setExcludes(excludes.split(","));
313                }
314            }});
315            archiver.setDestFile(file);
316
317            archiver.createArchive();
318        }
319        catch ( NoSuchArchiverException e ) {
320            throw new MojoExecutionException("Unknown archiver type", e);
321        }
322        catch (Exception e) {
323            e.printStackTrace();
324            throw new MojoExecutionException(
325                "Error packing directory: " + location + " to: " + file + "\r\n" + e.toString(), e );
326        }
327    }
328
329    /**
330     * 
331     */
332    private void setupRequest(final InvocationRequest req,
333                              final String additionalArguments) throws MojoExecutionException {
334        try {
335            final String[] args = CommandLineUtils.translateCommandline(additionalArguments);
336            CommandLine cli = new PosixParser().parse(OPTIONS, args);
337
338            if (cli.hasOption( SET_SYSTEM_PROPERTY))
339            {
340                String[] properties = cli.getOptionValues( SET_SYSTEM_PROPERTY );
341                Properties props = new Properties();
342                for ( int i = 0; i < properties.length; i++ )
343                {
344                    String property = properties[i];
345                    String name, value;
346                    int sep = property.indexOf( "=" );
347                    if ( sep <= 0 )
348                    {
349                        name = property.trim();
350                        value = "true";
351                    }
352                    else
353                    {
354                        name = property.substring( 0, sep ).trim();
355                        value = property.substring( sep + 1 ).trim();
356                    }
357                    props.setProperty( name, value );
358                }
359
360                req.setProperties( props );
361            }
362
363            if ( cli.hasOption( OFFLINE ) )
364            {
365                req.setOffline( true );
366            }
367
368            if ( cli.hasOption( QUIET ) )
369            {
370                // TODO: setQuiet() currently not supported by InvocationRequest
371                req.setDebug( false );
372            }
373            else if ( cli.hasOption( DEBUG ) )
374            {
375                req.setDebug( true );
376            }
377            else if ( cli.hasOption( ERRORS ) )
378            {
379                req.setShowErrors( true );
380            }
381
382            if ( cli.hasOption( REACTOR ) )
383            {
384                req.setRecursive( true );
385            }
386            else if ( cli.hasOption( NON_RECURSIVE ) )
387            {
388                req.setRecursive( false );
389            }
390
391            if ( cli.hasOption( UPDATE_SNAPSHOTS ) )
392            {
393                req.setUpdateSnapshots( true );
394            }
395
396            if ( cli.hasOption( ACTIVATE_PROFILES ) )
397            {
398                String[] profiles = cli.getOptionValues( ACTIVATE_PROFILES );
399                List<String> activatedProfiles = new ArrayList<String>();
400                List<String> deactivatedProfiles = new ArrayList<String>();
401
402                if ( profiles != null )
403                {
404                    for ( int i = 0; i < profiles.length; ++i )
405                    {
406                        StringTokenizer profileTokens = new StringTokenizer( profiles[i], "," );
407
408                        while ( profileTokens.hasMoreTokens() )
409                        {
410                            String profileAction = profileTokens.nextToken().trim();
411
412                            if ( profileAction.startsWith( "-" ) || profileAction.startsWith( "!" ) )
413                            {
414                                deactivatedProfiles.add( profileAction.substring( 1 ) );
415                            }
416                            else if ( profileAction.startsWith( "+" ) )
417                            {
418                                activatedProfiles.add( profileAction.substring( 1 ) );
419                            }
420                            else
421                            {
422                                activatedProfiles.add( profileAction );
423                            }
424                        }
425                    }
426                }
427
428                if (!deactivatedProfiles.isEmpty()) {
429                    getCaller().getLog().warn("Explicit profile deactivation is not yet supported. "
430                                              + "The following profiles will NOT be deactivated: " + StringUtils.join(
431                                                  deactivatedProfiles.iterator(), ", "));
432                }
433
434                if (!activatedProfiles.isEmpty()) {
435                    req.setProfiles(activatedProfiles);
436                }
437            }
438
439            if (cli.hasOption(FORCE_PLUGIN_UPDATES) || cli.hasOption( FORCE_PLUGIN_UPDATES2)) {
440                getCaller().getLog().warn("Forcing plugin updates is not supported currently.");
441            }
442            else if (cli.hasOption( SUPPRESS_PLUGIN_UPDATES)) {
443                req.setNonPluginUpdates( true );
444            }
445
446            if (cli.hasOption( SUPPRESS_PLUGIN_REGISTRY)) {
447                getCaller().getLog().warn("Explicit suppression of the plugin registry is not supported currently." );
448            }
449
450            if (cli.hasOption(CHECKSUM_FAILURE_POLICY)) {
451                req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_FAIL );
452            }
453            else if ( cli.hasOption( CHECKSUM_WARNING_POLICY ) )
454            {
455                req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_WARN );
456            }
457
458            if ( cli.hasOption( ALTERNATE_USER_SETTINGS ) )
459            {
460                req.setUserSettingsFile( new File( cli.getOptionValue( ALTERNATE_USER_SETTINGS ) ) );
461            }
462
463            if ( cli.hasOption( FAIL_AT_END ) )
464            {
465                req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_AT_END );
466            }
467            else if ( cli.hasOption( FAIL_FAST ) )
468            {
469                req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_FAST );
470            }
471            if ( cli.hasOption( FAIL_NEVER ) )
472            {
473                req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_NEVER );
474            }
475            if ( cli.hasOption( ALTERNATE_POM_FILE ) )
476            {
477                if (req.getPomFileName() != null) {
478                    getCaller().getLog().info("pomFileName is already set, ignoring the -f argument" );
479                }
480                else {
481                    req.setPomFileName( cli.getOptionValue( ALTERNATE_POM_FILE ) );
482                }
483            }
484        }
485        catch ( Exception e ) {
486            throw new MojoExecutionException("Failed to re-parse additional arguments for Maven invocation.", e );
487        }
488    }
489    
490    /**
491     * Executes the {@code install-file} goal with the new pom against the artifact file.
492     * 
493     * @param artifact {@link File} instance to install
494     */
495    public void installArtifact(final File artifact, 
496                                final File sources,
497                                final File mavenHome,
498                                final String groupId,
499                                final String artifactId,
500                                final String version,
501                                final String repositoryId) throws MojoExecutionException {
502        extractBuildXml();
503    
504        filterTempPom(groupId, artifactId, artifact.getName().endsWith("jar") ? "jar" : "war", version);
505
506        final Invoker invoker = new DefaultInvoker().setMavenHome(mavenHome);
507        
508        final String additionalArguments = "";
509
510        getCaller().getLog().debug("Setting up properties for installing the artifact");
511        final InvocationRequest req = new DefaultInvocationRequest()
512            .setInteractive(false)
513            .setProperties(new Properties() {{
514                setProperty("pomFile", getTempPomPath());
515                if (repositoryId != null) {
516                    setProperty("repositoryId", repositoryId);
517                }
518                if (sources != null) {
519                    try {
520                        setProperty("sources", zipSourcesIfRequired(sources).getCanonicalPath());
521                    }
522                    catch (Exception e) {
523                        throw new MojoExecutionException("Cannot get path for the sources file ", e);
524                    }                            
525                }
526                try {
527                    setProperty("file", artifact.getCanonicalPath());
528                }
529                catch (Exception e) {
530                    throw new MojoExecutionException("Cannot get path for the war file ", e);
531                }
532                setProperty("updateReleaseInfo", "true");
533            }});
534
535        getCaller().getLog().debug("Properties used for installArtifact are:");
536        try {
537            req.getProperties().list(System.out);
538        }
539        catch (Exception e) {
540        }
541
542        try {
543            setupRequest(req, additionalArguments);
544
545            if (repositoryId == null) {
546                req.setGoals(new ArrayList<String>() {{ add("install:install-file"); }});
547            }
548            else {
549                req.setGoals(new ArrayList<String>() {{ add("deploy:deploy-file"); }});
550            }
551
552            try {
553                final InvocationResult invocationResult = invoker.execute(req);
554
555                if ( invocationResult.getExecutionException() != null ) {
556                    throw new MojoExecutionException("Error executing Maven.",
557                                                     invocationResult.getExecutionException());
558                }
559                    
560                if (invocationResult.getExitCode() != 0) {
561                    throw new MojoExecutionException(
562                        "Maven execution failed, exit code: \'" + invocationResult.getExitCode() + "\'");
563                }
564            }
565            catch (MavenInvocationException e) {
566                throw new MojoExecutionException( "Failed to invoke Maven build.", e );
567            }
568        }
569        finally {
570            /*
571              if ( settingsFile != null && settingsFile.exists() && !settingsFile.delete() )
572              {
573              settingsFile.deleteOnExit();
574              }
575            */
576        }
577    }
578
579    /**
580     * Executes the {@code install-file} goal with the new pom against the artifact file.
581     * 
582     * @param artifact {@link File} instance to install
583     */
584    public void filterTempPom(final String groupId,
585                              final String artifactId,
586                              final String packaging,
587                              final String version) throws MojoExecutionException {
588        getCaller().getLog().info("Extracting the Temp POM");
589    
590        try {
591            executeCommandLine("ant",
592                               "-Dsource=" + System.getProperty("java.io.tmpdir") + File.separator + "pom.xml",
593                               "-Dtarget=" + System.getProperty("java.io.tmpdir") + File.separator + "prototype-pom.xml",
594                               "-DgroupId=" +  groupId,
595                               "-DartifactId=" + artifactId,
596                               "-Dpackaging=" + packaging,
597                               "-Dversion=" + version);
598        }
599        catch (Exception e) {
600            throw new MojoExecutionException("Error trying to filter the pom with ant ", e);
601        }
602    }
603
604    /**
605     * Temporary POM location
606     * 
607     * @return String value the path of the temporary POM
608     */
609    protected String getTempPomPath() {
610        return System.getProperty("java.io.tmpdir") + File.separator + "prototype-pom.xml";
611    }
612    
613    /**
614     * Puts ant build file in the system temp directory. build.xml is extracted
615     * from the plugin.
616     */
617    public void extractBuildXml() throws MojoExecutionException {
618        getCaller().getLog().info("Extracting the build.xml");
619        
620        final InputStream pom_is = getClass().getClassLoader().getResourceAsStream("prototype-resources/build.xml");
621        
622        byte[] fileBytes = null;
623        try {
624            final DataInputStream dis = new DataInputStream(pom_is);
625            fileBytes = new byte[dis.available()];
626            dis.readFully(fileBytes);
627            dis.close();
628        }
629        catch (Exception e) {
630            throw new MojoExecutionException("Wasn't able to read in the prototype pom", e);
631        }
632        finally {
633            try {
634                pom_is.close();
635            }
636            catch (Exception e) {
637                // Ignore exceptions
638            }
639        }
640        
641        try {
642            final FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + File.separator + "build.xml");
643            try {
644                fos.write(fileBytes);
645            }
646            finally {
647                fos.close();
648            }
649        }
650        catch (Exception e) {
651            throw new MojoExecutionException("Could not write temporary pom file", e);
652        }
653    }
654
655    /**
656     * Puts temporary pom in the system temp directory. prototype-pom.xml is extracted
657     * from the plugin.
658     */
659    public void extractTempPom() throws MojoExecutionException {
660        getCaller().getLog().info("Extracting the Temp Pom");
661        
662        final InputStream pom_is = getClass().getClassLoader().getResourceAsStream("prototype-resources/pom.xml");
663        
664        byte[] fileBytes = null;
665        try {
666            final DataInputStream dis = new DataInputStream(pom_is);
667            fileBytes = new byte[dis.available()];
668            dis.readFully(fileBytes);
669            dis.close();
670        }
671        catch (Exception e) {
672            throw new MojoExecutionException("Wasn't able to read in the prototype pom", e);
673        }
674        finally {
675            try {
676                pom_is.close();
677            }
678            catch (Exception e) {
679                // Ignore exceptions
680            }
681        }
682        
683        try {
684            final FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + File.separator + "pom.xml");
685            try {
686                fos.write(fileBytes);
687            }
688            finally {
689                fos.close();
690            }
691        }
692        catch (Exception e) {
693            throw new MojoExecutionException("Could not write temporary pom file", e);
694        }
695    }
696    
697    protected File zipSourcesIfRequired(File sources) {
698        if (sources.isFile()){
699            //Already a zip
700            return sources;
701        }
702        final File zipFile = new File(System.getProperty("java.io.tmpdir") + File.separator + "sources.zip");
703        zipFile.deleteOnExit();
704        ZipArchiver zipArchiver = new ZipArchiver();
705        zipArchiver.addDirectory(sources, new String[]{"**/*"}, new String[]{});
706        zipArchiver.setDestFile(zipFile);
707        try {
708            zipArchiver.createArchive();
709        } catch (IOException e) {
710            throw new RuntimeException("Unable to zip source directory", e);
711        }
712        return zipFile;
713    }
714
715    public void setCaller(final AbstractMojo caller) {
716        this.caller = caller;
717    }
718    
719    public AbstractMojo getCaller() {
720        return this.caller;
721    }
722
723}