001package com.pingidentity.util;
002
003import com.sun.istack.NotNull;
004import com.unboundid.directory.sdk.common.api.ManageExtensionPlugin;
005import com.unboundid.directory.sdk.common.types.ExtensionBundle;
006import com.unboundid.directory.sdk.common.types.InstallExtensionDetails;
007import com.unboundid.directory.sdk.common.types.PostManageExtensionPluginResult;
008
009import java.io.File;
010import java.io.IOException;
011import java.nio.charset.Charset;
012import java.nio.charset.StandardCharsets;
013import java.nio.file.*;
014import java.nio.file.attribute.BasicFileAttributes;
015import java.nio.file.attribute.FileTime;
016
017import static java.nio.file.FileVisitResult.CONTINUE;
018import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
019import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
020
021/**
022 * Created by arnaudlacour on 5/17/17.
023 */
024public class Installer extends ManageExtensionPlugin
025{
026    /**
027     * Copy source file to target location. If {@code prompt} is true then
028     * prompt user to overwrite target if it exists. The {@code preserve}
029     * parameter determines if file attributes should be copied/preserved.
030     */
031    static void copyFile(Path source, Path target)
032    {
033        CopyOption[] options = new CopyOption[]{REPLACE_EXISTING};
034        if (Files.notExists(target))
035        {
036            try
037            {
038                Files.copy(source, target, options);
039            } catch (IOException x)
040            {
041                System.err.format("Unable to copy: %s: %s%n", source, x);
042            }
043        }
044    }
045    
046    @Override
047    public String getExtensionName()
048    {
049        return "Generic Extension Installer";
050    }
051    
052    @Override
053    public String[] getExtensionDescription()
054    {
055        return new String[]{"Installs the extension this installer is bundled with"};
056    }
057    
058    @Override
059    public PostManageExtensionPluginResult postInstall(
060            InstallExtensionDetails details)
061    {
062        File instanceRoot = details.getExtensionBundle().getExtensionBundleDir().getParentFile().getParentFile();
063        
064        /* copy the documentation so it can be readily available for users */
065        copyDocumentation(instanceRoot, details);
066        
067        /* Update the docs index to add a link for convenience */
068        updateIndex(instanceRoot, details);
069        
070        /* if there is a browser to be opened, attempt to open the documentation directly */
071        openBrowser(details.getExtensionBundle());
072        
073        String confBatch = details.getExtensionBundle().getConfigDir() + "/config/install.dsconfig";
074        System.out.println("You can get started more quickly by running this command: ");
075        System.out.println(instanceRoot.toString() + "/bin/dsconfig --no-prompt --batch-file " + confBatch);
076        System.out.println("Executing this command will create basic configuration objects.");
077        
078        return PostManageExtensionPluginResult.SUCCESS;
079    }
080    
081    /**
082     * This method performs the necessary processing to attempt opening a browser window for the user at
083     * installation time and pop the documentation up.
084     *
085     * @param bundle the extension bundle to get the information from
086     */
087    private void openBrowser(final ExtensionBundle bundle)
088    {
089        String docUrl = "file://" + bundle.getDocumentationDir() + "/index.html";
090        String OS = System.getProperty("os.name").toLowerCase();
091        try
092        {
093            if (OS.contains("mac"))
094            {
095                Runtime.getRuntime().exec("open " + docUrl);
096            } else if (OS.contains("win"))
097            {
098                Runtime.getRuntime().exec("cmd /c start " + docUrl);
099            } else if (OS.contains("nux"))
100            {
101                Runtime.getRuntime().exec("xdg-open " + docUrl);
102            }
103        } catch (IOException e)
104        {
105        }
106    }
107    
108    /**
109     * This method performs the necessary processing to attempt copying the documentation bundled with the extension
110     * into a folder that can be reached by the docs servlet
111     *
112     * @param instanceRoot     the instance installation root file
113     * @param extensionDetails the extension detail information
114     */
115    private void copyDocumentation(@NotNull final File instanceRoot, @NotNull InstallExtensionDetails extensionDetails)
116    {
117        System.out.println("Deploying documentation set");
118        
119        String extensionNewDocRoot = instanceRoot + "/docs/extensions/" + extensionDetails.getExtensionBundle()
120                .getBundleId();
121        
122        File newDocDir = new File(extensionNewDocRoot);
123        newDocDir.mkdirs();
124        
125        Path srcDocPath = Paths.get(extensionDetails.getExtensionBundle().getDocumentationDir().getPath());
126        Path dstDocPath = Paths.get(extensionNewDocRoot);
127        
128        TreeCopier tc = new TreeCopier(srcDocPath, dstDocPath);
129        try
130        {
131            Files.walkFileTree(srcDocPath, tc);
132        } catch (IOException e)
133        {
134            System.out.println(e);
135        }
136    }
137    
138    /**
139     * This method performs the necessary processing to attempt updating the default documentation html page to add a
140     * direct link to the freshly copied extension documentation.
141     * Avoids duplicating links when updating the same extension version, for example during development
142     *
143     * @param extensionDetails the bundle details
144     */
145    private void updateIndex(@NotNull final File syncRoot, @NotNull InstallExtensionDetails extensionDetails)
146    {
147        Path path = Paths.get(syncRoot.getPath() + "/docs/index.html");
148        Charset charset = StandardCharsets.UTF_8;
149        
150        String content;
151        try
152        {
153            content = new String(Files.readAllBytes(path), charset);
154            String bundleId = extensionDetails.getExtensionBundle().getBundleId();
155            if (!content.contains("extensions/" + bundleId))
156            {
157                content = content.replaceAll("product.", "product.\n<BR><BR><BR><A HREF=\"extensions/" + bundleId +
158                        "/\">" + extensionDetails.getExtensionBundle().getTitle() + " " + extensionDetails
159                        .getExtensionBundle().getVersion
160                                () + "</A>");
161                Files.write(path, content.getBytes(charset));
162            }
163        } catch (IOException e)
164        {
165            System.out.println(e);
166        }
167    }
168    
169    /**
170     * A {@code FileVisitor} that copies a file-tree ("cp -r")
171     */
172    static class TreeCopier implements FileVisitor<Path>
173    {
174        private final Path source;
175        private final Path target;
176        
177        TreeCopier(Path source, Path target)
178        {
179            this.source = source;
180            this.target = target;
181        }
182        
183        @Override
184        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
185        {
186            // before visiting entries in a directory we copy the directory
187            // (okay if directory already exists).
188            CopyOption[] options = new CopyOption[0];
189            
190            Path newdir = target.resolve(source.relativize(dir));
191            try
192            {
193                Files.copy(dir, newdir, options);
194            } catch (FileAlreadyExistsException x)
195            {
196                // ignore
197            } catch (IOException x)
198            {
199                System.err.format("Unable to create: %s: %s%n", newdir, x);
200                return SKIP_SUBTREE;
201            }
202            return CONTINUE;
203        }
204        
205        @Override
206        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
207        {
208            copyFile(file, target.resolve(source.relativize(file)));
209            return CONTINUE;
210        }
211        
212        @Override
213        public FileVisitResult postVisitDirectory(Path dir, IOException exc)
214        {
215            // fix up modification time of directory when done
216            if (exc == null)
217            {
218                Path newdir = target.resolve(source.relativize(dir));
219                try
220                {
221                    FileTime time = Files.getLastModifiedTime(dir);
222                    Files.setLastModifiedTime(newdir, time);
223                } catch (IOException x)
224                {
225                    System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x);
226                }
227            }
228            return CONTINUE;
229        }
230        
231        @Override
232        public FileVisitResult visitFileFailed(Path file, IOException exc)
233        {
234            if (exc instanceof FileSystemLoopException)
235            {
236                System.err.println("cycle detected: " + file);
237            } else
238            {
239                System.err.format("Unable to copy: %s: %s%n", file, exc);
240            }
241            return CONTINUE;
242        }
243    }
244}