001package com.pingidentity.util;
002
003import com.unboundid.directory.sdk.common.api.ManageExtensionPlugin;
004import com.unboundid.directory.sdk.common.types.ExtensionBundle;
005import com.unboundid.directory.sdk.common.types.InstallExtensionDetails;
006import com.unboundid.directory.sdk.common.types.PostManageExtensionPluginResult;
007import com.unboundid.directory.sdk.common.types.UpdateExtensionDetails;
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    /**
047     * Returns the extension name
048     *
049     * @return the extension name
050     */
051    @Override
052    public String getExtensionName()
053    {
054        return "Generic Extension Installer";
055    }
056    
057    /**
058     * Provides descriptions for the extension
059     *
060     * @return an array of descriptive paragraphs
061     */
062    @Override
063    public String[] getExtensionDescription()
064    {
065        return new String[]{"Installs the extension this installer is bundled with"};
066    }
067    
068    /**
069     * Performs the necessary processing after the extension has been updated
070     *
071     * @param details the extension bundle details
072     * @return SUCCESS if the update was successful
073     */
074    @Override
075    public PostManageExtensionPluginResult postUpdate(UpdateExtensionDetails details)
076    {
077        File instanceRoot = details.getExtensionBundle().getExtensionBundleDir().getParentFile().getParentFile();
078        String confBatch = details.getExtensionBundle().getConfigDir() + "/update.dsconfig";
079        File configurationBatchFile = new File(confBatch);
080        if (configurationBatchFile.exists() && configurationBatchFile.isFile())
081        {
082            System.out.println();
083            System.out.println("You can get started more quickly by running this command: ");
084            System.out.println(instanceRoot.toString() + "/bin/dsconfig --no-prompt --batch-file " + confBatch);
085        }
086        return PostManageExtensionPluginResult.SUCCESS;
087    }
088    
089    /**
090     * Performs the necessary processing after the extension has been installed
091     * This currently includes things like laying down schema files, updating documentation, attempting to open
092     * a browser to the extension documentation page and displaying the appropriate commands to run
093     *
094     * @param details the extension bundle details
095     * @return SUCCESS if the install was successful
096     */
097    @Override
098    public PostManageExtensionPluginResult postInstall(
099            InstallExtensionDetails details)
100    {
101        File instanceRoot = details.getExtensionBundle().getExtensionBundleDir().getParentFile().getParentFile();
102        
103        /* copy schema files included in the extension if any */
104        copySchema(details, instanceRoot);
105        
106        /* copy the documentation so it can be readily available for users */
107        copyDocumentation(instanceRoot, details);
108        
109        /* Update the docs index to add a link for convenience */
110        updateIndex(instanceRoot, details);
111        
112        /* if there is a browser to be opened, attempt to open the documentation directly */
113        openBrowser(details.getExtensionBundle());
114        
115        
116        String confBatch = details.getExtensionBundle().getConfigDir() + "/install.dsconfig";
117        File configurationBatchFile = new File(confBatch);
118        if (configurationBatchFile.exists() && configurationBatchFile.isFile())
119        {
120            System.out.println();
121            System.out.println("You can get started more quickly by running this command: ");
122            System.out.println(instanceRoot.toString() + "/bin/dsconfig --no-prompt --batch-file " + confBatch);
123            System.out.println("Executing this command will create basic configuration objects.");
124        }
125        return PostManageExtensionPluginResult.SUCCESS;
126    }
127    
128    /**
129     * This method performs the necessary processing to copy schema to the instance
130     *
131     * @param details      the extension details
132     * @param instanceRoot the instance root
133     */
134    private void copySchema(final InstallExtensionDetails details, File instanceRoot)
135    {
136        System.out.println();
137        System.out.println("Schema preparation");
138        String schemaAddress = details.getExtensionBundle().getConfigDir() + "/schema";
139        File schemaDirectory = new File(schemaAddress);
140        if (schemaDirectory.exists() && schemaDirectory.isDirectory())
141        {
142            Path schemaSourcePath = Paths.get(schemaAddress);
143            try
144            {
145                DirectoryStream<Path>
146                        schemaDirectoryStream = Files.newDirectoryStream(
147                        schemaSourcePath, "??-*.ldif");
148                for (Path schemaFile : schemaDirectoryStream)
149                {
150                    try
151                    {
152                        System.out.println("Copying schema file " + schemaFile.toString());
153                        Files.copy(schemaFile, Paths.get(instanceRoot + "/config/schema/" + schemaFile.getFileName())
154                                , REPLACE_EXISTING);
155                    } catch (IOException e)
156                    {
157                        System.out.println(e.getMessage());
158                    }
159                }
160            } catch (IOException e)
161            {
162                System.out.println(e.getMessage());
163            }
164        }
165        System.out.println("Schema preparation: done.");
166    }
167    
168    /**
169     * This method performs the necessary processing to attempt opening a browser window for the user at
170     * installation time and pop the documentation up.
171     *
172     * @param bundle the extension bundle to get the information from
173     */
174    private void openBrowser(final ExtensionBundle bundle)
175    {
176        System.out.println();
177        System.out.println("Documentation convenience");
178        String docUrl = "file://" + bundle.getDocumentationDir() + "/index.html";
179        System.out.println("You may find the documentation for this extension here:");
180        System.out.println(docUrl);
181        String OS = System.getProperty("os.name").toLowerCase();
182        try
183        {
184            if (OS.contains("mac"))
185            {
186                Runtime.getRuntime().exec("open " + docUrl);
187            } else if (OS.contains("win"))
188            {
189                Runtime.getRuntime().exec("cmd /c start " + docUrl);
190            } else if (OS.contains("nux"))
191            {
192                Runtime.getRuntime().exec("xdg-open " + docUrl);
193            }
194        } catch (IOException e)
195        {
196        }
197        System.out.println("Documentation convenience: done.");
198    }
199    
200    /**
201     * This method performs the necessary processing to attempt copying the documentation bundled with the extension
202     * into a folder that can be reached by the docs servlet
203     *
204     * @param instanceRoot     the instance installation root file
205     * @param extensionDetails the extension detail information
206     */
207    private void copyDocumentation(final File instanceRoot,
208                                   InstallExtensionDetails extensionDetails)
209    {
210        System.out.println();
211        System.out.println("Documentation preparation");
212        
213        String extensionNewDocRoot = instanceRoot + "/docs/extensions/" + extensionDetails.getExtensionBundle()
214                .getBundleId();
215        
216        File newDocDir = new File(extensionNewDocRoot);
217        newDocDir.mkdirs();
218        
219        Path srcDocPath = Paths.get(extensionDetails.getExtensionBundle().getDocumentationDir().getPath());
220        Path dstDocPath = Paths.get(extensionNewDocRoot);
221        
222        TreeCopier tc = new TreeCopier(srcDocPath, dstDocPath);
223        try
224        {
225            Files.walkFileTree(srcDocPath, tc);
226        } catch (IOException e)
227        {
228            System.out.println(e);
229        }
230        System.out.println("Documentation preparation: done.");
231    }
232    
233    /**
234     * This method performs the necessary processing to attempt updating the default documentation html page to
235     * add a
236     * direct link to the freshly copied extension documentation.
237     * Avoids duplicating links when updating the same extension version, for example during development
238     *
239     * @param extensionDetails the bundle details
240     */
241    private void updateIndex(final File syncRoot, InstallExtensionDetails extensionDetails)
242    {
243        System.out.println();
244        System.out.println("Documentation index update");
245        
246        Path path = Paths.get(syncRoot.getPath() + "/docs/index.html");
247        if ( path.toFile().exists() ) {
248            Charset charset = StandardCharsets.UTF_8;
249            String content;
250            try {
251                content = new String(Files.readAllBytes(path), charset);
252                String bundleId = extensionDetails.getExtensionBundle().getBundleId();
253                if (!content.contains("extensions/" + bundleId)) {
254                    content = content.replaceAll("product.", "product.\n<BR><BR><BR><A HREF=\"extensions/" + bundleId +
255                            "/\">" + extensionDetails.getExtensionBundle().getTitle() + " " + extensionDetails
256                            .getExtensionBundle().getVersion
257                                    () + "</A>");
258                    Files.write(path, content.getBytes(charset));
259                }
260            } catch (IOException e) {
261                System.out.println(e);
262            }
263            System.out.println("Documentation index update: done.");
264        } else {
265            System.out.println("Documentation index update: skipped.");
266        }
267    }
268    
269    /**
270     * A {@code FileVisitor} that copies a file-tree ("cp -r")
271     */
272    static class TreeCopier implements FileVisitor<Path>
273    {
274        private final Path source;
275        private final Path target;
276        
277        TreeCopier(Path source, Path target)
278        {
279            this.source = source;
280            this.target = target;
281        }
282        
283        @Override
284        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
285        {
286            // before visiting entries in a directory we copy the directory
287            // (okay if directory already exists).
288            CopyOption[] options = new CopyOption[0];
289            
290            Path newdir = target.resolve(source.relativize(dir));
291            try
292            {
293                Files.copy(dir, newdir, options);
294            } catch (FileAlreadyExistsException x)
295            {
296                // ignore
297            } catch (IOException x)
298            {
299                System.err.format("Unable to create: %s: %s%n", newdir, x);
300                return SKIP_SUBTREE;
301            }
302            return CONTINUE;
303        }
304        
305        @Override
306        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
307        {
308            copyFile(file, target.resolve(source.relativize(file)));
309            return CONTINUE;
310        }
311        
312        @Override
313        public FileVisitResult postVisitDirectory(Path dir, IOException exc)
314        {
315            // fix up modification time of directory when done
316            if (exc == null)
317            {
318                Path newdir = target.resolve(source.relativize(dir));
319                try
320                {
321                    FileTime time = Files.getLastModifiedTime(dir);
322                    Files.setLastModifiedTime(newdir, time);
323                } catch (IOException x)
324                {
325                    System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x);
326                }
327            }
328            return CONTINUE;
329        }
330        
331        @Override
332        public FileVisitResult visitFileFailed(Path file, IOException exc)
333        {
334            if (exc instanceof FileSystemLoopException)
335            {
336                System.err.println("cycle detected: " + file);
337            } else
338            {
339                System.err.format("Unable to copy: %s: %s%n", file, exc);
340            }
341            return CONTINUE;
342        }
343    }
344}