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