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 */ 019package org.apache.directory.server.core.changelog; 020 021 022import java.io.BufferedReader; 023import java.io.File; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.ObjectInputStream; 027import java.io.ObjectOutputStream; 028import java.io.OutputStream; 029import java.io.PrintWriter; 030import java.nio.charset.StandardCharsets; 031import java.nio.file.Files; 032import java.util.ArrayList; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.Properties; 038 039import org.apache.directory.api.ldap.model.cursor.Cursor; 040import org.apache.directory.api.ldap.model.cursor.ListCursor; 041import org.apache.directory.api.ldap.model.exception.LdapException; 042import org.apache.directory.api.ldap.model.ldif.LdifEntry; 043import org.apache.directory.api.util.DateUtils; 044import org.apache.directory.api.util.TimeProvider; 045import org.apache.directory.server.core.api.DirectoryService; 046import org.apache.directory.server.core.api.LdapPrincipal; 047import org.apache.directory.server.core.api.changelog.ChangeLogEvent; 048import org.apache.directory.server.core.api.changelog.ChangeLogEventSerializer; 049import org.apache.directory.server.core.api.changelog.Tag; 050import org.apache.directory.server.core.api.changelog.TaggableChangeLogStore; 051import org.apache.directory.server.i18n.I18n; 052 053 054/** 055 * A change log store that keeps it's information in memory. 056 * 057 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 058 */ 059public class MemoryChangeLogStore implements TaggableChangeLogStore 060{ 061 062 private static final String REV_FILE = "revision"; 063 private static final String TAG_FILE = "tags"; 064 private static final String CHANGELOG_FILE = "changelog.dat"; 065 066 /** An incremental number giving the current revision */ 067 private long currentRevision; 068 069 /** The latest tag */ 070 private Tag latest; 071 072 /** A Map of tags and revisions */ 073 private final Map<Long, Tag> tags = new HashMap<>( 100 ); 074 075 private final List<ChangeLogEvent> events = new ArrayList<>(); 076 private File workingDirectory; 077 078 /** The DirectoryService */ 079 private DirectoryService directoryService; 080 081 private TimeProvider timeProvider = TimeProvider.DEFAULT; 082 083 /** 084 * {@inheritDoc} 085 */ 086 @Override 087 public Tag tag( long revision ) 088 { 089 if ( tags.containsKey( revision ) ) 090 { 091 return tags.get( revision ); 092 } 093 094 latest = new Tag( revision, null ); 095 tags.put( revision, latest ); 096 097 return latest; 098 } 099 100 101 /** 102 * {@inheritDoc} 103 */ 104 @Override 105 public Tag tag() 106 { 107 if ( ( latest != null ) && ( latest.getRevision() == currentRevision ) ) 108 { 109 return latest; 110 } 111 112 latest = new Tag( currentRevision, null ); 113 tags.put( currentRevision, latest ); 114 return latest; 115 } 116 117 118 /** 119 * {@inheritDoc} 120 */ 121 @Override 122 public Tag tag( String description ) 123 { 124 if ( ( latest != null ) && ( latest.getRevision() == currentRevision ) ) 125 { 126 return latest; 127 } 128 129 latest = new Tag( currentRevision, description ); 130 tags.put( currentRevision, latest ); 131 return latest; 132 } 133 134 135 /** 136 * {@inheritDoc} 137 */ 138 @Override 139 public void init( DirectoryService service ) throws LdapException 140 { 141 workingDirectory = service.getInstanceLayout().getLogDirectory(); 142 this.directoryService = service; 143 this.timeProvider = service.getTimeProvider(); 144 145 try 146 { 147 loadRevision(); 148 loadTags(); 149 loadChangeLog(); 150 } 151 catch ( IOException ioe ) 152 { 153 throw new LdapException( ioe.getMessage(), ioe ); 154 } 155 } 156 157 158 // This will suppress PMD.EmptyCatchBlock warnings in this method 159 private void loadRevision() throws IOException 160 { 161 File revFile = new File( workingDirectory, REV_FILE ); 162 163 if ( revFile.exists() ) 164 { 165 try ( BufferedReader reader = Files.newBufferedReader( revFile.toPath(), StandardCharsets.UTF_8 ) ) 166 { 167 String line = reader.readLine(); 168 currentRevision = Long.parseLong( line ); 169 } 170 } 171 } 172 173 174 private void saveRevision() throws IOException 175 { 176 File revFile = new File( workingDirectory, REV_FILE ); 177 178 if ( revFile.exists() && !revFile.delete() ) 179 { 180 throw new IOException( I18n.err( I18n.ERR_726_FILE_UNDELETABLE, revFile.getAbsolutePath() ) ); 181 } 182 183 184 try ( PrintWriter out = new PrintWriter( Files.newBufferedWriter( revFile.toPath(), StandardCharsets.UTF_8 ) ) ) 185 { 186 out.println( currentRevision ); 187 out.flush(); 188 } 189 } 190 191 192 // This will suppress PMD.EmptyCatchBlock warnings in this method 193 private void saveTags() throws IOException 194 { 195 File tagFile = new File( workingDirectory, TAG_FILE ); 196 197 if ( tagFile.exists() && !tagFile.delete() ) 198 { 199 throw new IOException( I18n.err( I18n.ERR_726_FILE_UNDELETABLE, tagFile.getAbsolutePath() ) ); 200 } 201 202 OutputStream out = null; 203 204 try 205 { 206 out = Files.newOutputStream( tagFile.toPath() ); 207 208 Properties props = new Properties(); 209 210 for ( Tag tag : tags.values() ) 211 { 212 String key = String.valueOf( tag.getRevision() ); 213 214 if ( tag.getDescription() == null ) 215 { 216 props.setProperty( key, "null" ); 217 } 218 else 219 { 220 props.setProperty( key, tag.getDescription() ); 221 } 222 } 223 224 props.store( out, null ); 225 out.flush(); 226 } 227 finally 228 { 229 if ( out != null ) 230 { 231 out.close(); 232 } 233 } 234 } 235 236 237 // This will suppress PMD.EmptyCatchBlock warnings in this method 238 private void loadTags() throws IOException 239 { 240 File revFile = new File( workingDirectory, REV_FILE ); 241 242 if ( revFile.exists() ) 243 { 244 Properties props = new Properties(); 245 InputStream in = null; 246 247 try 248 { 249 in = Files.newInputStream( revFile.toPath() ); 250 props.load( in ); 251 ArrayList<Long> revList = new ArrayList<>(); 252 253 for ( Object key : props.keySet() ) 254 { 255 revList.add( Long.valueOf( ( String ) key ) ); 256 } 257 258 Collections.sort( revList ); 259 Tag tag = null; 260 261 // @todo need some serious syncrhoization here on tags 262 tags.clear(); 263 264 for ( Long lkey : revList ) 265 { 266 String rev = String.valueOf( lkey ); 267 String desc = props.getProperty( rev ); 268 269 if ( ( desc != null ) && desc.equals( "null" ) ) 270 { 271 tag = new Tag( lkey, null ); 272 } 273 else 274 { 275 tag = new Tag( lkey, desc ); 276 } 277 278 tags.put( lkey, tag ); 279 } 280 281 latest = tag; 282 } 283 finally 284 { 285 if ( in != null ) 286 { 287 in.close(); 288 } 289 } 290 } 291 } 292 293 294 // This will suppress PMD.EmptyCatchBlock warnings in this method 295 private void loadChangeLog() throws IOException 296 { 297 File file = new File( workingDirectory, CHANGELOG_FILE ); 298 299 if ( file.exists() ) 300 { 301 try ( ObjectInputStream in = new ObjectInputStream( Files.newInputStream( file.toPath() ) ) ) 302 { 303 int size = in.readInt(); 304 305 ArrayList<ChangeLogEvent> changeLogEvents = new ArrayList<>( size ); 306 307 for ( int i = 0; i < size; i++ ) 308 { 309 ChangeLogEvent event = ChangeLogEventSerializer.deserialize( directoryService.getSchemaManager(), 310 in ); 311 event.getCommitterPrincipal().setSchemaManager( directoryService.getSchemaManager() ); 312 changeLogEvents.add( event ); 313 } 314 315 // @todo man o man we need some synchronization later after getting this to work 316 this.events.clear(); 317 this.events.addAll( changeLogEvents ); 318 } 319 } 320 } 321 322 323 // This will suppress PMD.EmptyCatchBlock warnings in this method 324 private void saveChangeLog() throws IOException 325 { 326 File file = new File( workingDirectory, CHANGELOG_FILE ); 327 328 if ( file.exists() && !file.delete() ) 329 { 330 throw new IOException( I18n.err( I18n.ERR_726_FILE_UNDELETABLE, file.getAbsolutePath() ) ); 331 } 332 333 file.createNewFile(); 334 335 try ( ObjectOutputStream out = new ObjectOutputStream( Files.newOutputStream( file.toPath() ) ) ) 336 { 337 out.writeInt( events.size() ); 338 339 for ( ChangeLogEvent event : events ) 340 { 341 ChangeLogEventSerializer.serialize( event, out ); 342 } 343 344 out.flush(); 345 } 346 } 347 348 349 /** 350 * {@inheritDoc} 351 */ 352 @Override 353 public void sync() throws LdapException 354 { 355 try 356 { 357 saveRevision(); 358 saveTags(); 359 saveChangeLog(); 360 } 361 catch ( IOException ioe ) 362 { 363 throw new LdapException( ioe.getMessage(), ioe ); 364 } 365 } 366 367 368 /** 369 * Save logs, tags and revision on disk, and clean everything in memory 370 */ 371 @Override 372 public void destroy() throws LdapException 373 { 374 try 375 { 376 saveRevision(); 377 saveTags(); 378 saveChangeLog(); 379 } 380 catch ( IOException ioe ) 381 { 382 throw new LdapException( ioe.getMessage(), ioe ); 383 } 384 } 385 386 387 /** 388 * {@inheritDoc} 389 */ 390 @Override 391 public long getCurrentRevision() 392 { 393 return currentRevision; 394 } 395 396 397 /** 398 * {@inheritDoc} 399 */ 400 @Override 401 public ChangeLogEvent log( LdapPrincipal principal, LdifEntry forward, LdifEntry reverse ) 402 { 403 currentRevision++; 404 ChangeLogEvent event = new ChangeLogEvent( currentRevision, 405 DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ), 406 principal, forward, reverse ); 407 events.add( event ); 408 409 return event; 410 } 411 412 413 /** 414 * {@inheritDoc} 415 */ 416 @Override 417 public ChangeLogEvent log( LdapPrincipal principal, LdifEntry forward, List<LdifEntry> reverses ) 418 { 419 currentRevision++; 420 ChangeLogEvent event = new ChangeLogEvent( currentRevision, 421 DateUtils.getGeneralizedTime( timeProvider ), principal, forward, reverses ); 422 events.add( event ); 423 424 return event; 425 } 426 427 428 /** 429 * {@inheritDoc} 430 */ 431 @Override 432 public ChangeLogEvent lookup( long revision ) 433 { 434 if ( revision < 0 ) 435 { 436 throw new IllegalArgumentException( I18n.err( I18n.ERR_239 ) ); 437 } 438 439 if ( revision > getCurrentRevision() ) 440 { 441 throw new IllegalArgumentException( I18n.err( I18n.ERR_240 ) ); 442 } 443 444 return events.get( ( int ) revision ); 445 } 446 447 448 /** 449 * {@inheritDoc} 450 */ 451 @Override 452 public Cursor<ChangeLogEvent> find() 453 { 454 return new ListCursor<>( events ); 455 } 456 457 458 /** 459 * {@inheritDoc} 460 */ 461 @Override 462 public Cursor<ChangeLogEvent> findBefore( long revision ) 463 { 464 return new ListCursor<>( events, ( int ) revision ); 465 } 466 467 468 /** 469 * {@inheritDoc} 470 */ 471 @Override 472 public Cursor<ChangeLogEvent> findAfter( long revision ) 473 { 474 return new ListCursor<>( ( int ) revision, events ); 475 } 476 477 478 /** 479 * {@inheritDoc} 480 */ 481 @Override 482 public Cursor<ChangeLogEvent> find( long startRevision, long endRevision ) 483 { 484 return new ListCursor<>( ( int ) startRevision, events, ( int ) ( endRevision + 1 ) ); 485 } 486 487 488 /** 489 * {@inheritDoc} 490 */ 491 @Override 492 public Tag getLatest() 493 { 494 return latest; 495 } 496 497 498 /** 499 * {@inheritDoc} 500 */ 501 @Override 502 public Tag removeTag( long revision ) 503 { 504 return tags.remove( revision ); 505 } 506 507 508 /** 509 * {@inheritDoc} 510 */ 511 @Override 512 public Tag tag( long revision, String descrition ) 513 { 514 if ( tags.containsKey( revision ) ) 515 { 516 return tags.get( revision ); 517 } 518 519 latest = new Tag( revision, descrition ); 520 tags.put( revision, latest ); 521 return latest; 522 } 523 524 525 /** 526 * @see Object#toString() 527 */ 528 @Override 529 public String toString() 530 { 531 StringBuilder sb = new StringBuilder(); 532 533 sb.append( "MemoryChangeLog\n" ); 534 sb.append( "latest tag : " ).append( latest ).append( '\n' ); 535 536 sb.append( "Nb of events : " ).append( events.size() ).append( '\n' ); 537 538 int i = 0; 539 540 for ( ChangeLogEvent event : events ) 541 { 542 sb.append( "event[" ).append( i++ ).append( "] : " ); 543 sb.append( "\n---------------------------------------\n" ); 544 sb.append( event ); 545 sb.append( "\n---------------------------------------\n" ); 546 } 547 548 return sb.toString(); 549 } 550}