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}