View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *  
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *  
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License. 
18   *  
19   */
20  package org.apache.directory.api.ldap.schema.extractor.impl;
21  
22  
23  import java.io.File;
24  import java.io.FileNotFoundException;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InvalidObjectException;
28  import java.io.OutputStream;
29  import java.io.OutputStreamWriter;
30  import java.io.Writer;
31  import java.net.URL;
32  import java.nio.charset.Charset;
33  import java.nio.file.Files;
34  import java.nio.file.Paths;
35  import java.util.ArrayDeque;
36  import java.util.Deque;
37  import java.util.Enumeration;
38  import java.util.Map;
39  import java.util.Map.Entry;
40  import java.util.UUID;
41  import java.util.regex.Pattern;
42  
43  import org.apache.directory.api.i18n.I18n;
44  import org.apache.directory.api.ldap.model.constants.SchemaConstants;
45  import org.apache.directory.api.ldap.model.exception.LdapException;
46  import org.apache.directory.api.ldap.model.ldif.LdifEntry;
47  import org.apache.directory.api.ldap.model.ldif.LdifReader;
48  import org.apache.directory.api.ldap.schema.extractor.SchemaLdifExtractor;
49  import org.apache.directory.api.ldap.schema.extractor.UniqueResourceException;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  
54  /**
55   * Extracts LDIF files for the schema repository onto a destination directory.
56   *
57   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
58   */
59  public class DefaultSchemaLdifExtractor implements SchemaLdifExtractor
60  {
61      /** The base path. */
62      private static final String BASE_PATH = "";
63  
64      /** The schema sub-directory. */
65      private static final String SCHEMA_SUBDIR = "schema";
66  
67      /** The logger. */
68      private static final Logger LOG = LoggerFactory.getLogger( DefaultSchemaLdifExtractor.class );
69  
70      /**
71       * The pattern to extract the schema from LDIF files.
72       * java.util.regex.Pattern is immutable so only one instance is needed for all uses.
73       */
74      private static final Pattern EXTRACT_PATTERN = Pattern.compile( ".*schema" + "[/\\Q\\\\E]" + "ou=schema.*\\.ldif" );
75  
76      /** The extracted flag. */
77      private boolean extracted;
78  
79      /** The output directory. */
80      private File outputDirectory;
81  
82  
83      /**
84       * Creates an extractor which deposits files into the specified output
85       * directory.
86       *
87       * @param outputDirectory the directory where the schema root is extracted
88       */
89      public DefaultSchemaLdifExtractor( File outputDirectory )
90      {
91          LOG.debug( "BASE_PATH set to {}, outputDirectory set to {}", BASE_PATH, outputDirectory );
92          this.outputDirectory = outputDirectory;
93          File schemaDirectory = new File( outputDirectory, SCHEMA_SUBDIR );
94  
95          if ( !outputDirectory.exists() )
96          {
97              LOG.debug( "Creating output directory: {}", outputDirectory );
98              if ( !outputDirectory.mkdir() )
99              {
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 }