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   *    https://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          if ( LOG.isDebugEnabled() )
92          {
93              LOG.debug( I18n.msg( I18n.MSG_16000_BASE_PATH, BASE_PATH, outputDirectory ) );
94          }
95          
96          this.outputDirectory = outputDirectory;
97          File schemaDirectory = new File( outputDirectory, SCHEMA_SUBDIR );
98  
99          if ( !outputDirectory.exists() )
100         {
101             if ( LOG.isDebugEnabled() )
102             {
103                 LOG.debug( I18n.msg( I18n.MSG_16001_CREATING_DIR, outputDirectory ) );
104             }
105             
106             if ( !outputDirectory.mkdir() )
107             {
108                 LOG.error( I18n.err( I18n.ERR_16042_OUTPUT_DIR_CREATION_FAIL, outputDirectory ) );
109             }
110         }
111         else
112         {
113             if ( LOG.isDebugEnabled() )
114             {
115                 LOG.debug( I18n.msg( I18n.MSG_16002_DIR_EXISTS ) );
116             }
117         }
118 
119         if ( !schemaDirectory.exists() )
120         {
121             if ( LOG.isInfoEnabled() )
122             {
123                 LOG.info( I18n.msg( I18n.MSG_16004_SCHEMA_DIR_ABSENT, schemaDirectory ) );
124             }
125             
126             extracted = false;
127         }
128         else
129         {
130             if ( LOG.isInfoEnabled() )
131             {
132                 LOG.info( I18n.msg( I18n.MSG_16005_SCHEMA_DIR_PRESENT, schemaDirectory ) );
133             }
134             
135             extracted = true;
136         }
137     }
138 
139 
140     /**
141      * Gets whether or not schema folder has been created or not.
142      *
143      * @return true if schema folder has already been extracted.
144      */
145     @Override
146     public boolean isExtracted()
147     {
148         return extracted;
149     }
150 
151 
152     /**
153      * Extracts the LDIF files from a Jar file or copies exploded LDIF resources.
154      *
155      * @param overwrite over write extracted structure if true, false otherwise
156      * @throws IOException if schema already extracted and on IO errors
157      */
158     @Override
159     public void extractOrCopy( boolean overwrite ) throws IOException
160     {
161         if ( !outputDirectory.exists() && !outputDirectory.mkdirs() )
162         {
163             throw new IOException( I18n.err( I18n.ERR_16006_DIRECTORY_CREATION_FAILED, outputDirectory
164                 .getAbsolutePath() ) );
165         }
166 
167         File schemaDirectory = new File( outputDirectory, SCHEMA_SUBDIR );
168 
169         if ( !schemaDirectory.exists() )
170         {
171             if ( !schemaDirectory.mkdirs() )
172             {
173                 throw new IOException( I18n.err( I18n.ERR_16006_DIRECTORY_CREATION_FAILED, schemaDirectory
174                     .getAbsolutePath() ) );
175             }
176         }
177         else if ( !overwrite )
178         {
179             throw new IOException( I18n.err( I18n.ERR_16000_CANNOT_OVEWRITE_SCHEMA, schemaDirectory.getAbsolutePath() ) );
180         }
181 
182         Map<String, Boolean> list = ResourceMap.getResources( EXTRACT_PATTERN );
183 
184         for ( Entry<String, Boolean> entry : list.entrySet() )
185         {
186             if ( entry.getValue() )
187             {
188                 extractFromClassLoader( entry.getKey() );
189             }
190             else
191             {
192                 File resource = new File( entry.getKey() );
193                 copyFile( resource, getDestinationFile( resource ) );
194             }
195         }
196     }
197 
198 
199     /**
200      * Extracts the LDIF files from a Jar file or copies exploded LDIF
201      * resources without overwriting the resources if the schema has
202      * already been extracted.
203      *
204      * @throws IOException if schema already extracted and on IO errors
205      */
206     @Override
207     public void extractOrCopy() throws IOException
208     {
209         extractOrCopy( false );
210     }
211 
212 
213     /**
214      * Copies a file line by line from the source file argument to the 
215      * destination file argument.
216      *
217      * @param source the source file to copy
218      * @param destination the destination to copy the source to
219      * @throws IOException if there are IO errors or the source does not exist
220      */
221     private void copyFile( File source, File destination ) throws IOException
222     {
223         if ( LOG.isDebugEnabled() )
224         {
225             LOG.debug( I18n.msg( I18n.MSG_16003_COPYFILE, source, destination ) );
226         }
227 
228         if ( !destination.getParentFile().exists() && !destination.getParentFile().mkdirs() )
229         {
230             throw new IOException( I18n.err( I18n.ERR_16006_DIRECTORY_CREATION_FAILED, destination.getParentFile()
231                 .getAbsolutePath() ) );
232         }
233 
234         if ( !source.getParentFile().exists() )
235         {
236             throw new FileNotFoundException( I18n.err( I18n.ERR_16001_CANNOT_COPY_NON_EXISTENT, source.getAbsolutePath() ) );
237         }
238 
239         try ( Writer out = new OutputStreamWriter( Files.newOutputStream( Paths.get( destination.getPath() ) ), 
240             Charset.defaultCharset() );
241             LdifReader ldifReader = new LdifReader( source ) )
242         {
243             boolean first = true;
244             LdifEntry ldifEntry = null;
245 
246             while ( ldifReader.hasNext() )
247             {
248                 if ( first )
249                 {
250                     ldifEntry = ldifReader.next();
251 
252                     if ( ldifEntry.get( SchemaConstants.ENTRY_UUID_AT ) == null )
253                     {
254                         // No UUID, let's create one
255                         UUID entryUuid = UUID.randomUUID();
256                         ldifEntry.addAttribute( SchemaConstants.ENTRY_UUID_AT, entryUuid.toString() );
257                     }
258 
259                     first = false;
260                 }
261                 else
262                 {
263                     // throw an exception : we should not have more than one entry per schema ldif file
264                     String msg = I18n.err( I18n.ERR_16002_MORE_THAN_ONE_ENTRY, source );
265                     LOG.error( msg );
266                     throw new InvalidObjectException( msg );
267                 }
268             }
269 
270             // Add the version at the first line, to avoid a warning
271             String ldifString;
272             
273             if ( ldifEntry != null )
274             {
275                 ldifString = "version: 1\n" + ldifEntry.toString();
276             }
277             else
278             {
279                 ldifString = "version: 1\n";
280             }
281 
282             out.write( ldifString );
283             out.flush();
284         }
285         catch ( LdapException le )
286         {
287             String msg = I18n.err( I18n.ERR_16003_ERROR_PARSING_LDIF, source, le.getLocalizedMessage() );
288             LOG.error( msg );
289             throw new InvalidObjectException( msg );
290         }
291     }
292 
293 
294     /**
295      * Assembles the destination file by appending file components previously
296      * pushed on the fileComponentStack argument.
297      *
298      * @param fileComponentStack stack containing pushed file components
299      * @return the assembled destination file
300      */
301     private File assembleDestinationFile( Deque<String> fileComponentStack )
302     {
303         File destinationFile = outputDirectory.getAbsoluteFile();
304 
305         while ( !fileComponentStack.isEmpty() )
306         {
307             destinationFile = new File( destinationFile, fileComponentStack.pop() );
308         }
309 
310         return destinationFile;
311     }
312 
313 
314     /**
315      * Calculates the destination file.
316      *
317      * @param resource the source file
318      * @return the destination file's parent directory
319      */
320     private File getDestinationFile( File resource )
321     {
322         File parent = resource.getParentFile();
323         Deque<String> fileComponentStack = new ArrayDeque<>();
324         fileComponentStack.push( resource.getName() );
325 
326         while ( parent != null )
327         {
328             if ( "schema".equals( parent.getName() ) )
329             {
330                 // All LDIF files besides the schema.ldif are under the 
331                 // schema/schema base path. So we need to add one more 
332                 // schema component to all LDIF files minus this schema.ldif
333                 fileComponentStack.push( "schema" );
334 
335                 return assembleDestinationFile( fileComponentStack );
336             }
337 
338             fileComponentStack.push( parent.getName() );
339 
340             if ( parent.equals( parent.getParentFile() ) || parent.getParentFile() == null )
341             {
342                 throw new IllegalStateException( I18n.err( I18n.ERR_16004_ROOT_WITHOUT_SCHEMA ) );
343             }
344 
345             parent = parent.getParentFile();
346         }
347 
348         throw new IllegalStateException( I18n.err( I18n.ERR_16005_PARENT_NULL ) );
349     }
350 
351 
352     /**
353      * Gets the unique schema file resource from the class loader off the base path.  If 
354      * the same resource exists multiple times then an error will result since the resource
355      * is not unique.
356      *
357      * @param resourceName the file name of the resource to load
358      * @param resourceDescription human description of the resource
359      * @return the InputStream to read the contents of the resource
360      * @throws IOException if there are problems reading or finding a unique copy of the resource
361      */
362     public static InputStream getUniqueResourceAsStream( String resourceName, String resourceDescription )
363         throws IOException
364     {
365         URL result = getUniqueResource( BASE_PATH + resourceName, resourceDescription );
366         
367         return result.openStream();
368     }
369 
370 
371     /**
372      * Gets a unique resource from the class loader.
373      * 
374      * @param resourceName the name of the resource
375      * @param resourceDescription the description of the resource
376      * @return the URL to the resource in the class loader
377      * @throws IOException if there is an IO error
378      */
379     public static URL getUniqueResource( String resourceName, String resourceDescription ) throws IOException
380     {
381         Enumeration<URL> resources = DefaultSchemaLdifExtractor.class.getClassLoader().getResources( resourceName );
382         if ( !resources.hasMoreElements() )
383         {
384             throw new UniqueResourceException( resourceName, resourceDescription );
385         }
386         URL result = resources.nextElement();
387         if ( resources.hasMoreElements() )
388         {
389             throw new UniqueResourceException( resourceName, result, resources, resourceDescription );
390         }
391         return result;
392     }
393 
394 
395     /**
396      * Gets resource from the class loader.
397      * In case of several files with the same name, it returns any of them.
398      * This is useful in cases when the same artefacts are loaded several times, e.g. in some testing scenarios.
399      *
400      * @param resourceName the name of the resource
401      * @param resourceDescription the description of the resource
402      * @return the URL to the resource in the class loader
403      * @throws IOException if there is an IO error
404      */
405     public static URL getAnyResource( String resourceName, String resourceDescription ) throws IOException
406     {
407         Enumeration<URL> resources = DefaultSchemaLdifExtractor.class.getClassLoader().getResources( resourceName );
408         if ( !resources.hasMoreElements() )
409         {
410             throw new UniqueResourceException( resourceName, resourceDescription );
411         }
412         URL result = resources.nextElement();
413         return result;
414     }
415 
416     /**
417      * Extracts the LDIF schema resource from class loader.
418      *
419      * @param resource the LDIF schema resource
420      * @throws IOException if there are IO errors
421      */
422     private void extractFromClassLoader( String resource ) throws IOException
423     {
424         byte[] buf = new byte[512];
425         
426         try ( InputStream in = DefaultSchemaLdifExtractor.getUniqueResourceAsStream( resource,
427             "LDIF file in schema repository" ) )
428         {
429             File destination = new File( outputDirectory, resource );
430 
431             /*
432              * Do not overwrite an LDIF file if it has already been extracted.
433              */
434             if ( destination.exists() )
435             {
436                 return;
437             }
438 
439             if ( !destination.getParentFile().exists() && !destination.getParentFile().mkdirs() )
440             {
441                 throw new IOException( I18n.err( I18n.ERR_16006_DIRECTORY_CREATION_FAILED, destination
442                     .getParentFile().getAbsolutePath() ) );
443             }
444 
445             try ( OutputStream out = Files.newOutputStream( Paths.get( destination.getPath() ) ) )
446             
447             {
448                 while ( in.available() > 0 )
449                 {
450                     int readCount = in.read( buf );
451                     out.write( buf, 0, readCount );
452                 }
453 
454                 out.flush();
455             }
456         }
457     }
458 }