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