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