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}