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  package org.apache.directory.server.core.changelog;
20  
21  
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.ObjectInputStream;
27  import java.io.ObjectOutputStream;
28  import java.io.OutputStream;
29  import java.io.PrintWriter;
30  import java.nio.charset.StandardCharsets;
31  import java.nio.file.Files;
32  import java.util.ArrayList;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Properties;
38  
39  import org.apache.directory.api.ldap.model.cursor.Cursor;
40  import org.apache.directory.api.ldap.model.cursor.ListCursor;
41  import org.apache.directory.api.ldap.model.exception.LdapException;
42  import org.apache.directory.api.ldap.model.ldif.LdifEntry;
43  import org.apache.directory.api.util.DateUtils;
44  import org.apache.directory.api.util.TimeProvider;
45  import org.apache.directory.server.core.api.DirectoryService;
46  import org.apache.directory.server.core.api.LdapPrincipal;
47  import org.apache.directory.server.core.api.changelog.ChangeLogEvent;
48  import org.apache.directory.server.core.api.changelog.ChangeLogEventSerializer;
49  import org.apache.directory.server.core.api.changelog.Tag;
50  import org.apache.directory.server.core.api.changelog.TaggableChangeLogStore;
51  import org.apache.directory.server.i18n.I18n;
52  
53  
54  /**
55   * A change log store that keeps it's information in memory.
56   *
57   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
58   */
59  public class MemoryChangeLogStore implements TaggableChangeLogStore
60  {
61  
62      private static final String REV_FILE = "revision";
63      private static final String TAG_FILE = "tags";
64      private static final String CHANGELOG_FILE = "changelog.dat";
65  
66      /** An incremental number giving the current revision */
67      private long currentRevision;
68  
69      /** The latest tag */
70      private Tag latest;
71  
72      /** A Map of tags and revisions */
73      private final Map<Long, Tag> tags = new HashMap<>( 100 );
74  
75      private final List<ChangeLogEvent> events = new ArrayList<>();
76      private File workingDirectory;
77  
78      /** The DirectoryService */
79      private DirectoryService directoryService;
80  
81      private TimeProvider timeProvider = TimeProvider.DEFAULT;
82  
83      /**
84       * {@inheritDoc}
85       */
86      @Override
87      public Tag tag( long revision )
88      {
89          if ( tags.containsKey( revision ) )
90          {
91              return tags.get( revision );
92          }
93  
94          latest = new Tag( revision, null );
95          tags.put( revision, latest );
96          
97          return latest;
98      }
99  
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         ChangeLogEventore/api/changelog/ChangeLogEvent.html#ChangeLogEvent">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         ChangeLogEventore/api/changelog/ChangeLogEvent.html#ChangeLogEvent">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 }