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 */
020package org.apache.directory.api.ldap.schema.extractor.impl;
021
022
023import java.io.File;
024import java.io.FileNotFoundException;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InvalidObjectException;
028import java.io.OutputStream;
029import java.io.OutputStreamWriter;
030import java.io.Writer;
031import java.net.URL;
032import java.nio.charset.Charset;
033import java.nio.file.Files;
034import java.nio.file.Paths;
035import java.util.ArrayDeque;
036import java.util.Deque;
037import java.util.Enumeration;
038import java.util.Map;
039import java.util.Map.Entry;
040import java.util.UUID;
041import java.util.regex.Pattern;
042
043import org.apache.directory.api.i18n.I18n;
044import org.apache.directory.api.ldap.model.constants.SchemaConstants;
045import org.apache.directory.api.ldap.model.exception.LdapException;
046import org.apache.directory.api.ldap.model.ldif.LdifEntry;
047import org.apache.directory.api.ldap.model.ldif.LdifReader;
048import org.apache.directory.api.ldap.schema.extractor.SchemaLdifExtractor;
049import org.apache.directory.api.ldap.schema.extractor.UniqueResourceException;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053
054/**
055 * Extracts LDIF files for the schema repository onto a destination directory.
056 *
057 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
058 */
059public class DefaultSchemaLdifExtractor implements SchemaLdifExtractor
060{
061    /** The base path. */
062    private static final String BASE_PATH = "";
063
064    /** The schema sub-directory. */
065    private static final String SCHEMA_SUBDIR = "schema";
066
067    /** The logger. */
068    private static final Logger LOG = LoggerFactory.getLogger( DefaultSchemaLdifExtractor.class );
069
070    /**
071     * The pattern to extract the schema from LDIF files.
072     * java.util.regex.Pattern is immutable so only one instance is needed for all uses.
073     */
074    private static final Pattern EXTRACT_PATTERN = Pattern.compile( ".*schema" + "[/\\Q\\\\E]" + "ou=schema.*\\.ldif" );
075
076    /** The extracted flag. */
077    private boolean extracted;
078
079    /** The output directory. */
080    private File outputDirectory;
081
082
083    /**
084     * Creates an extractor which deposits files into the specified output
085     * directory.
086     *
087     * @param outputDirectory the directory where the schema root is extracted
088     */
089    public DefaultSchemaLdifExtractor( File outputDirectory )
090    {
091        LOG.debug( "BASE_PATH set to {}, outputDirectory set to {}", BASE_PATH, outputDirectory );
092        this.outputDirectory = outputDirectory;
093        File schemaDirectory = new File( outputDirectory, SCHEMA_SUBDIR );
094
095        if ( !outputDirectory.exists() )
096        {
097            LOG.debug( "Creating output directory: {}", outputDirectory );
098            if ( !outputDirectory.mkdir() )
099            {
100                LOG.error( "Failed to create outputDirectory: {}", outputDirectory );
101            }
102        }
103        else
104        {
105            LOG.debug( "Output directory exists: no need to create." );
106        }
107
108        if ( !schemaDirectory.exists() )
109        {
110            LOG.info( "Schema directory '{}' does NOT exist: extracted state set to false.", schemaDirectory );
111            extracted = false;
112        }
113        else
114        {
115            LOG.info( "Schema directory '{}' does exist: extracted state set to true.", schemaDirectory );
116            extracted = true;
117        }
118    }
119
120
121    /**
122     * Gets whether or not schema folder has been created or not.
123     *
124     * @return true if schema folder has already been extracted.
125     */
126    @Override
127    public boolean isExtracted()
128    {
129        return extracted;
130    }
131
132
133    /**
134     * Extracts the LDIF files from a Jar file or copies exploded LDIF resources.
135     *
136     * @param overwrite over write extracted structure if true, false otherwise
137     * @throws IOException if schema already extracted and on IO errors
138     */
139    @Override
140    public void extractOrCopy( boolean overwrite ) throws IOException
141    {
142        if ( !outputDirectory.exists() && !outputDirectory.mkdirs() )
143        {
144            throw new IOException( I18n.err( I18n.ERR_09001_DIRECTORY_CREATION_FAILED, outputDirectory
145                .getAbsolutePath() ) );
146        }
147
148        File schemaDirectory = new File( outputDirectory, SCHEMA_SUBDIR );
149
150        if ( !schemaDirectory.exists() )
151        {
152            if ( !schemaDirectory.mkdirs() )
153            {
154                throw new IOException( I18n.err( I18n.ERR_09001_DIRECTORY_CREATION_FAILED, schemaDirectory
155                    .getAbsolutePath() ) );
156            }
157        }
158        else if ( !overwrite )
159        {
160            throw new IOException( I18n.err( I18n.ERR_08001, schemaDirectory.getAbsolutePath() ) );
161        }
162
163        Map<String, Boolean> list = ResourceMap.getResources( EXTRACT_PATTERN );
164
165        for ( Entry<String, Boolean> entry : list.entrySet() )
166        {
167            if ( entry.getValue() )
168            {
169                extractFromClassLoader( entry.getKey() );
170            }
171            else
172            {
173                File resource = new File( entry.getKey() );
174                copyFile( resource, getDestinationFile( resource ) );
175            }
176        }
177    }
178
179
180    /**
181     * Extracts the LDIF files from a Jar file or copies exploded LDIF
182     * resources without overwriting the resources if the schema has
183     * already been extracted.
184     *
185     * @throws IOException if schema already extracted and on IO errors
186     */
187    @Override
188    public void extractOrCopy() throws IOException
189    {
190        extractOrCopy( false );
191    }
192
193
194    /**
195     * Copies a file line by line from the source file argument to the 
196     * destination file argument.
197     *
198     * @param source the source file to copy
199     * @param destination the destination to copy the source to
200     * @throws IOException if there are IO errors or the source does not exist
201     */
202    private void copyFile( File source, File destination ) throws IOException
203    {
204        LOG.debug( "copyFile(): source = {}, destination = {}", source, destination );
205
206        if ( !destination.getParentFile().exists() && !destination.getParentFile().mkdirs() )
207        {
208            throw new IOException( I18n.err( I18n.ERR_09001_DIRECTORY_CREATION_FAILED, destination.getParentFile()
209                .getAbsolutePath() ) );
210        }
211
212        if ( !source.getParentFile().exists() )
213        {
214            throw new FileNotFoundException( I18n.err( I18n.ERR_08002, source.getAbsolutePath() ) );
215        }
216
217        try ( Writer out = new OutputStreamWriter( Files.newOutputStream( Paths.get( destination.getPath() ) ), 
218            Charset.defaultCharset() );
219            LdifReader ldifReader = new LdifReader( source ) )
220        {
221            boolean first = true;
222            LdifEntry ldifEntry = null;
223
224            while ( ldifReader.hasNext() )
225            {
226                if ( first )
227                {
228                    ldifEntry = ldifReader.next();
229
230                    if ( ldifEntry.get( SchemaConstants.ENTRY_UUID_AT ) == null )
231                    {
232                        // No UUID, let's create one
233                        UUID entryUuid = UUID.randomUUID();
234                        ldifEntry.addAttribute( SchemaConstants.ENTRY_UUID_AT, entryUuid.toString() );
235                    }
236
237                    first = false;
238                }
239                else
240                {
241                    // throw an exception : we should not have more than one entry per schema ldif file
242                    String msg = I18n.err( I18n.ERR_08003, source );
243                    LOG.error( msg );
244                    throw new InvalidObjectException( msg );
245                }
246            }
247
248            // Add the version at the first line, to avoid a warning
249            String ldifString;
250            
251            if ( ldifEntry != null )
252            {
253                ldifString = "version: 1\n" + ldifEntry.toString();
254            }
255            else
256            {
257                ldifString = "version: 1\n";
258            }
259
260            out.write( ldifString );
261            out.flush();
262        }
263        catch ( LdapException le )
264        {
265            String msg = I18n.err( I18n.ERR_08004, source, le.getLocalizedMessage() );
266            LOG.error( msg );
267            throw new InvalidObjectException( msg );
268        }
269    }
270
271
272    /**
273     * Assembles the destination file by appending file components previously
274     * pushed on the fileComponentStack argument.
275     *
276     * @param fileComponentStack stack containing pushed file components
277     * @return the assembled destination file
278     */
279    private File assembleDestinationFile( Deque<String> fileComponentStack )
280    {
281        File destinationFile = outputDirectory.getAbsoluteFile();
282
283        while ( !fileComponentStack.isEmpty() )
284        {
285            destinationFile = new File( destinationFile, fileComponentStack.pop() );
286        }
287
288        return destinationFile;
289    }
290
291
292    /**
293     * Calculates the destination file.
294     *
295     * @param resource the source file
296     * @return the destination file's parent directory
297     */
298    private File getDestinationFile( File resource )
299    {
300        File parent = resource.getParentFile();
301        Deque<String> fileComponentStack = new ArrayDeque<>();
302        fileComponentStack.push( resource.getName() );
303
304        while ( parent != null )
305        {
306            if ( "schema".equals( parent.getName() ) )
307            {
308                // All LDIF files besides the schema.ldif are under the 
309                // schema/schema base path. So we need to add one more 
310                // schema component to all LDIF files minus this schema.ldif
311                fileComponentStack.push( "schema" );
312
313                return assembleDestinationFile( fileComponentStack );
314            }
315
316            fileComponentStack.push( parent.getName() );
317
318            if ( parent.equals( parent.getParentFile() ) || parent.getParentFile() == null )
319            {
320                throw new IllegalStateException( I18n.err( I18n.ERR_08005 ) );
321            }
322
323            parent = parent.getParentFile();
324        }
325
326        throw new IllegalStateException( I18n.err( I18n.ERR_08006 ) );
327    }
328
329
330    /**
331     * Gets the unique schema file resource from the class loader off the base path.  If 
332     * the same resource exists multiple times then an error will result since the resource
333     * is not unique.
334     *
335     * @param resourceName the file name of the resource to load
336     * @param resourceDescription human description of the resource
337     * @return the InputStream to read the contents of the resource
338     * @throws IOException if there are problems reading or finding a unique copy of the resource
339     */
340    public static InputStream getUniqueResourceAsStream( String resourceName, String resourceDescription )
341        throws IOException
342    {
343        URL result = getUniqueResource( BASE_PATH + resourceName, resourceDescription );
344        return result.openStream();
345    }
346
347
348    /**
349     * Gets a unique resource from the class loader.
350     * 
351     * @param resourceName the name of the resource
352     * @param resourceDescription the description of the resource
353     * @return the URL to the resource in the class loader
354     * @throws IOException if there is an IO error
355     */
356    public static URL getUniqueResource( String resourceName, String resourceDescription ) throws IOException
357    {
358        Enumeration<URL> resources = DefaultSchemaLdifExtractor.class.getClassLoader().getResources( resourceName );
359        if ( !resources.hasMoreElements() )
360        {
361            throw new UniqueResourceException( resourceName, resourceDescription );
362        }
363        URL result = resources.nextElement();
364        if ( resources.hasMoreElements() )
365        {
366            throw new UniqueResourceException( resourceName, result, resources, resourceDescription );
367        }
368        return result;
369    }
370
371
372    /**
373     * Extracts the LDIF schema resource from class loader.
374     *
375     * @param resource the LDIF schema resource
376     * @throws IOException if there are IO errors
377     */
378    private void extractFromClassLoader( String resource ) throws IOException
379    {
380        byte[] buf = new byte[512];
381        InputStream in = DefaultSchemaLdifExtractor.getUniqueResourceAsStream( resource,
382            "LDIF file in schema repository" );
383
384        try
385        {
386            File destination = new File( outputDirectory, resource );
387
388            /*
389             * Do not overwrite an LDIF file if it has already been extracted.
390             */
391            if ( destination.exists() )
392            {
393                return;
394            }
395
396            if ( !destination.getParentFile().exists() && !destination.getParentFile().mkdirs() )
397            {
398                throw new IOException( I18n.err( I18n.ERR_09001_DIRECTORY_CREATION_FAILED, destination
399                    .getParentFile().getAbsolutePath() ) );
400            }
401
402            OutputStream out = Files.newOutputStream( Paths.get( destination.getPath() ) );
403            try
404            {
405                while ( in.available() > 0 )
406                {
407                    int readCount = in.read( buf );
408                    out.write( buf, 0, readCount );
409                }
410                out.flush();
411            }
412            finally
413            {
414                out.close();
415            }
416        }
417        finally
418        {
419            in.close();
420        }
421    }
422}