001/*
002 *   Licensed to the Apache Software Foundation (ASF) under one
003 *   or more contributor license agreements.  See the NOTICE file
004 *   distributed with this work for additional information
005 *   regarding copyright ownership.  The ASF licenses this file
006 *   to you under the Apache License, Version 2.0 (the
007 *   "License"); you may not use this file except in compliance
008 *   with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *   Unless required by applicable law or agreed to in writing,
013 *   software distributed under the License is distributed on an
014 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *   KIND, either express or implied.  See the License for the
016 *   specific language governing permissions and limitations
017 *   under the License.
018 *
019 */
020
021package org.apache.directory.server.config;
022
023
024import java.io.BufferedReader;
025import java.io.File;
026import java.io.FileNotFoundException;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.OutputStream;
030import java.io.Writer;
031import java.net.URL;
032import java.nio.charset.StandardCharsets;
033import java.nio.file.Files;
034import java.util.Map;
035import java.util.Map.Entry;
036import java.util.Stack;
037import java.util.regex.Pattern;
038
039import org.apache.directory.api.ldap.schema.extractor.impl.DefaultSchemaLdifExtractor;
040import org.apache.directory.api.ldap.schema.extractor.impl.ResourceMap;
041import org.apache.directory.api.util.Strings;
042import org.apache.directory.server.i18n.I18n;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046
047/**
048 * A class to copy the default config to the work directory of a DirectoryService instance.
049 * 
050 * NOTE: much of this class code is duplicated from DefaultSchemaLdifExtractor class
051 *       We should create a AbstractLdifExtractor class and move the reusable code there
052 * 
053 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
054 */
055public final class LdifConfigExtractor
056{
057
058    public static final String LDIF_CONFIG_FILE = "config.ldif";
059
060    private static final String CONFIG_SUBDIR = "config";
061
062    private static final Logger LOG = LoggerFactory.getLogger( LdifConfigExtractor.class );
063
064    // java.util.regex.Pattern is immutable so only one instance is needed for all uses.
065    private static final Pattern EXTRACT_PATTERN = Pattern.compile( ".*config"
066        + "[/\\Q\\\\E]" + "ou=config.*\\.ldif" );
067
068
069    private LdifConfigExtractor()
070    {
071    }
072
073
074    /**
075     * Extracts the LDIF files from a Jar file or copies exploded LDIF resources.
076     *
077     * @param outputDirectory The directory where to extract the configuration
078     * @param overwrite over write extracted structure if true, false otherwise
079     * @throws IOException if schema already extracted and on IO errors
080     */
081    public static void extract( File outputDirectory, boolean overwrite ) throws IOException
082    {
083        if ( !outputDirectory.exists() )
084        {
085            LOG.debug( "creating non existing output directory {}", outputDirectory.getAbsolutePath() );
086            if ( !outputDirectory.mkdir() )
087            {
088                throw new IOException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECTORY, outputDirectory ) );
089            }
090        }
091
092        File configDirectory = new File( outputDirectory, CONFIG_SUBDIR );
093
094        if ( !configDirectory.exists() )
095        {
096            LOG.debug( "creating non existing config directory {}", configDirectory.getAbsolutePath() );
097            if ( !configDirectory.mkdir() )
098            {
099                throw new IOException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECTORY, configDirectory ) );
100            }
101        }
102        else if ( !overwrite )
103        {
104            throw new IOException( I18n.err( I18n.ERR_508, configDirectory.getAbsolutePath() ) );
105        }
106
107        LOG.debug( "extracting the configuration to the directory at {}", configDirectory.getAbsolutePath() );
108
109        Map<String, Boolean> list = ResourceMap.getResources( EXTRACT_PATTERN );
110
111        for ( Entry<String, Boolean> entry : list.entrySet() )
112        {
113            if ( entry.getValue() )
114            {
115                extractFromJar( outputDirectory, entry.getKey() );
116            }
117            else
118            {
119                File resource = new File( entry.getKey() );
120                copyFile( resource, getDestinationFile( outputDirectory, resource ) );
121            }
122        }
123    }
124
125
126    /**
127     * Copies a file line by line from the source file argument to the
128     * destination file argument.
129     *
130     * @param source the source file to copy
131     * @param destination the destination to copy the source to
132     * @throws IOException if there are IO errors or the source does not exist
133     */
134    private static void copyFile( File source, File destination ) throws IOException
135    {
136        LOG.debug( "copyFile(): source = {}, destination = {}", source, destination );
137
138        if ( !destination.getParentFile().exists() && !destination.getParentFile().mkdirs() )
139        {
140            throw new IOException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECTORY, destination.getParentFile() ) );
141        }
142
143        if ( !source.getParentFile().exists() )
144        {
145            throw new FileNotFoundException( I18n.err( I18n.ERR_509, source.getAbsolutePath() ) );
146        }
147
148        try ( Writer out = Files.newBufferedWriter( destination.toPath(), StandardCharsets.UTF_8 );
149            BufferedReader in = Files.newBufferedReader( source.toPath(), StandardCharsets.UTF_8 ); )
150        {
151            String line;
152
153            while ( null != ( line = in.readLine() ) )
154            {
155                out.write( line + "\n" );
156            }
157
158            out.flush();
159        }
160    }
161
162
163    /**
164     * Extracts the LDIF schema resource from a Jar.
165     *
166     * @param resource the LDIF schema resource
167     * @throws IOException if there are IO errors
168     */
169    private static void extractFromJar( File outputDirectory, String resource ) throws IOException
170    {
171        byte[] buf = new byte[512];
172
173        try ( InputStream in = DefaultSchemaLdifExtractor.getUniqueResourceAsStream( resource,
174            "LDIF file in config repository" ) ) 
175        {
176            File destination = new File( outputDirectory, resource );
177
178            /*
179             * Do not overwrite an LDIF file if it has already been extracted.
180             */
181            if ( destination.exists() )
182            {
183                return;
184            }
185
186            if ( !destination.getParentFile().exists() && !destination.getParentFile().mkdirs() )
187            {
188                throw new IOException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECTORY,
189                    destination.getParentFile() ) );
190            }
191
192            try ( OutputStream out = Files.newOutputStream( destination.toPath() ) )
193            {
194                while ( in.available() > 0 )
195                {
196                    int readCount = in.read( buf );
197                    out.write( buf, 0, readCount );
198                }
199                out.flush();
200            }
201        }
202    }
203
204
205    /**
206     * Calculates the destination file.
207     *
208     * @param resource the source file
209     * @return the destination file's parent directory
210     */
211    private static File getDestinationFile( File outputDirectory, File resource )
212    {
213        File parent = resource.getParentFile();
214        Stack<String> fileComponentStack = new Stack<>();
215        fileComponentStack.push( resource.getName() );
216
217        while ( parent != null )
218        {
219            if ( parent.getName().equals( "config" ) )
220            {
221                // All LDIF files besides the config.ldif are under the
222                // config/config base path. So we need to add one more
223                // schema component to all LDIF files minus this config.ldif
224                fileComponentStack.push( "config" );
225
226                return assembleDestinationFile( outputDirectory, fileComponentStack );
227            }
228
229            fileComponentStack.push( parent.getName() );
230
231            if ( parent.equals( parent.getParentFile() ) || parent.getParentFile() == null )
232            {
233                throw new IllegalStateException( I18n.err( I18n.ERR_510 ) );
234            }
235
236            parent = parent.getParentFile();
237        }
238
239        throw new IllegalStateException( I18n.err( I18n.ERR_511 ) );
240    }
241
242
243    /**
244     * Assembles the destination file by appending file components previously
245     * pushed on the fileComponentStack argument.
246     *
247     * @param fileComponentStack stack containing pushed file components
248     * @return the assembled destination file
249     */
250    private static File assembleDestinationFile( File outputDirectory, Stack<String> fileComponentStack )
251    {
252        File destinationFile = outputDirectory.getAbsoluteFile();
253
254        while ( !fileComponentStack.isEmpty() )
255        {
256            destinationFile = new File( destinationFile, fileComponentStack.pop() );
257        }
258
259        return destinationFile;
260    }
261
262
263    /**
264     * extracts or overwrites the configuration LDIF file and returns the absolute path of this file
265     *
266     * @param configDir the directory where the config file should be extracted to
267     * @param file The file containing the configuration
268     * @param overwrite flag to indicate to overwrite the config file if already present in the given config directory
269     * @return complete path of the config file on disk
270     */
271    public static String extractSingleFileConfig( File configDir, String file, boolean overwrite )
272    {
273        if ( file == null )
274        {
275            file = LDIF_CONFIG_FILE;
276        }
277
278        File configFile = new File( configDir, file );
279
280        if ( !configDir.exists() )
281        {
282            LOG.debug( "creating non existing config directory {}", configDir.getAbsolutePath() );
283            if ( !configDir.mkdir() )
284            {
285                throw new RuntimeException(
286                    new IOException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECTORY, configDir ) ) );
287            }
288        }
289        else
290        {
291            if ( configFile.exists() && !overwrite )
292            {
293                LOG.warn( "config file already exists, returning, cause overwrite flag was set to false" );
294                return configFile.getAbsolutePath();
295            }
296        }
297
298        try
299        {
300
301            URL configUrl = LdifConfigExtractor.class.getClassLoader().getResource( file );
302
303            LOG.debug( "URL of the config ldif file {}", configUrl );
304
305            byte[] buf = new byte[1024 * 1024];
306
307            
308            try ( InputStream in = configUrl.openStream();
309                Writer writer = Files.newBufferedWriter( configFile.toPath(), StandardCharsets.UTF_8 ) )
310            {
311                while ( true )
312                {
313                    int read = in.read( buf );
314
315                    if ( read <= 0 )
316                    {
317                        break;
318                    }
319
320                    String s = Strings.utf8ToString( buf, 0, read );
321                    writer.write( s );
322                }
323            }
324
325            LOG.info( "successfully extracted the config file {}", configFile.getAbsoluteFile() );
326
327            return configFile.getAbsolutePath();
328        }
329        catch ( Exception e )
330        {
331            throw new RuntimeException( e );
332        }
333    }
334}