001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.server.protocols;
018
019import java.io.File;
020import java.io.IOException;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.LinkedHashMap;
024import java.util.List;
025
026import org.forgerock.i18n.LocalizableMessage;
027import org.forgerock.i18n.LocalizableMessageBuilder;
028import org.opends.server.admin.server.ConfigurationChangeListener;
029import org.opends.server.admin.std.server.ConnectionHandlerCfg;
030import org.opends.server.admin.std.server.LDIFConnectionHandlerCfg;
031import org.opends.server.api.AlertGenerator;
032import org.opends.server.api.ClientConnection;
033import org.opends.server.api.ConnectionHandler;
034import org.opends.server.core.DirectoryServer;
035import org.opends.server.core.ServerContext;
036import org.forgerock.i18n.slf4j.LocalizedLogger;
037import org.opends.server.protocols.internal.InternalClientConnection;
038import org.forgerock.opendj.config.server.ConfigChangeResult;
039import org.opends.server.types.DirectoryConfig;
040import org.forgerock.opendj.ldap.DN;
041import org.opends.server.types.ExistingFileBehavior;
042import org.opends.server.types.HostPort;
043import org.opends.server.types.LDIFExportConfig;
044import org.opends.server.types.LDIFImportConfig;
045import org.opends.server.types.Operation;
046import org.opends.server.util.AddChangeRecordEntry;
047import org.opends.server.util.ChangeRecordEntry;
048import org.opends.server.util.DeleteChangeRecordEntry;
049import org.opends.server.util.LDIFException;
050import org.opends.server.util.LDIFReader;
051import org.opends.server.util.LDIFWriter;
052import org.opends.server.util.ModifyChangeRecordEntry;
053import org.opends.server.util.ModifyDNChangeRecordEntry;
054import org.opends.server.util.TimeThread;
055
056import static org.opends.messages.ProtocolMessages.*;
057import static org.opends.server.util.ServerConstants.*;
058import static org.opends.server.util.StaticUtils.*;
059
060/**
061 * This class defines an LDIF connection handler, which can be used to watch for
062 * new LDIF files to be placed in a specified directory.  If a new LDIF file is
063 * detected, the connection handler will process any changes contained in that
064 * file as internal operations.
065 */
066public final class LDIFConnectionHandler
067       extends ConnectionHandler<LDIFConnectionHandlerCfg>
068       implements ConfigurationChangeListener<LDIFConnectionHandlerCfg>,
069                  AlertGenerator
070{
071  /** The debug log tracer for this class. */
072  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
073
074
075
076  /** Indicates whether this connection handler is currently stopped. */
077  private volatile boolean isStopped;
078
079  /** Indicates whether we should stop this connection handler. */
080  private volatile boolean stopRequested;
081
082  /** The path to the directory to watch for new LDIF files. */
083  private File ldifDirectory;
084
085  /** The internal client connection that will be used for all processing. */
086  private InternalClientConnection conn;
087
088  /** The current configuration for this LDIF connection handler. */
089  private LDIFConnectionHandlerCfg currentConfig;
090
091  /** The thread used to run the connection handler. */
092  private Thread connectionHandlerThread;
093
094  /** Help to not warn permanently and fullfill the log file in debug mode. */
095  private boolean alreadyWarn;
096
097
098  /**
099   * Creates a new instance of this connection handler.  All initialization
100   * should be performed in the {@code initializeConnectionHandler} method.
101   */
102  public LDIFConnectionHandler()
103  {
104    super("LDIFConnectionHandler");
105
106    isStopped               = true;
107    stopRequested           = false;
108    connectionHandlerThread = null;
109    alreadyWarn = false;
110  }
111
112
113
114  /** {@inheritDoc} */
115  @Override
116  public void initializeConnectionHandler(ServerContext serverContext, LDIFConnectionHandlerCfg
117                                               configuration)
118  {
119    String ldifDirectoryPath = configuration.getLDIFDirectory();
120    ldifDirectory = new File(ldifDirectoryPath);
121
122    // If we have a relative path to the instance, get the absolute one.
123    if ( ! ldifDirectory.isAbsolute() ) {
124      ldifDirectory = new File(DirectoryServer.getInstanceRoot()
125          + File.separator + ldifDirectoryPath);
126    }
127
128    if (ldifDirectory.exists())
129    {
130      if (! ldifDirectory.isDirectory())
131      {
132        // The path specified as the LDIF directory exists, but isn't a
133        // directory.  This is probably a mistake, and we should at least log
134        // a warning message.
135        logger.warn(WARN_LDIF_CONNHANDLER_LDIF_DIRECTORY_NOT_DIRECTORY,
136            ldifDirectory.getAbsolutePath(), configuration.dn());
137      }
138    }
139    else
140    {
141      // The path specified as the LDIF directory doesn't exist.  We should log
142      // a warning message saying that we won't do anything until it's created.
143      logger.warn(WARN_LDIF_CONNHANDLER_LDIF_DIRECTORY_MISSING,
144          ldifDirectory.getAbsolutePath(), configuration.dn());
145    }
146
147    this.currentConfig = configuration;
148    currentConfig.addLDIFChangeListener(this);
149    DirectoryConfig.registerAlertGenerator(this);
150    conn = InternalClientConnection.getRootConnection();
151  }
152
153
154
155  /** {@inheritDoc} */
156  @Override
157  public void finalizeConnectionHandler(LocalizableMessage finalizeReason)
158  {
159    stopRequested = true;
160
161    for (int i=0; i < 5; i++)
162    {
163      if (isStopped)
164      {
165        return;
166      }
167      else
168      {
169        try
170        {
171          if (connectionHandlerThread != null && connectionHandlerThread.isAlive())
172          {
173            connectionHandlerThread.join(100);
174            connectionHandlerThread.interrupt();
175          }
176          else
177          {
178            return;
179          }
180        } catch (Exception e) {}
181      }
182    }
183  }
184
185
186
187  /** {@inheritDoc} */
188  @Override
189  public String getConnectionHandlerName()
190  {
191    return "LDIF Connection Handler";
192  }
193
194
195
196  /** {@inheritDoc} */
197  @Override
198  public String getProtocol()
199  {
200    return "LDIF";
201  }
202
203
204
205  /** {@inheritDoc} */
206  @Override
207  public Collection<HostPort> getListeners()
208  {
209    // There are no listeners for this connection handler.
210    return Collections.<HostPort>emptySet();
211  }
212
213
214
215  /** {@inheritDoc} */
216  @Override
217  public Collection<ClientConnection> getClientConnections()
218  {
219    // There are no client connections for this connection handler.
220    return Collections.<ClientConnection>emptySet();
221  }
222
223
224
225  /** {@inheritDoc} */
226  @Override
227  public void run()
228  {
229    isStopped = false;
230    connectionHandlerThread = Thread.currentThread();
231
232    try
233    {
234      while (! stopRequested)
235      {
236        try
237        {
238          long startTime = System.currentTimeMillis();
239
240          File dir = ldifDirectory;
241          if (dir.exists() && dir.isDirectory())
242          {
243            File[] ldifFiles = dir.listFiles();
244            if (ldifFiles != null)
245            {
246              for (File f : ldifFiles)
247              {
248                if (f.getName().endsWith(".ldif"))
249                {
250                  processLDIFFile(f);
251                }
252              }
253            }
254          }
255          else
256          {
257            if (!alreadyWarn && logger.isTraceEnabled())
258            {
259              logger.trace("LDIF connection handler directory " +
260                               dir.getAbsolutePath() +
261                               " doesn't exist or isn't a directory");
262              alreadyWarn = true;
263            }
264          }
265
266          if (! stopRequested)
267          {
268            long currentTime = System.currentTimeMillis();
269            long sleepTime   = startTime + currentConfig.getPollInterval() -
270                               currentTime;
271            if (sleepTime > 0)
272            {
273              try
274              {
275                Thread.sleep(sleepTime);
276              }
277              catch (InterruptedException ie)
278              {
279                logger.traceException(ie);
280              }
281            }
282          }
283        }
284        catch (Exception e)
285        {
286          logger.traceException(e);
287        }
288      }
289    }
290    finally
291    {
292      connectionHandlerThread = null;
293      isStopped = true;
294    }
295  }
296
297
298
299  /**
300   * Processes the contents of the provided LDIF file.
301   *
302   * @param  ldifFile  The LDIF file to be processed.
303   */
304  private void processLDIFFile(File ldifFile)
305  {
306    if (logger.isTraceEnabled())
307    {
308      logger.trace("Beginning processing on LDIF file " +
309                       ldifFile.getAbsolutePath());
310    }
311
312    boolean fullyProcessed = false;
313    boolean errorEncountered = false;
314    String inputPath = ldifFile.getAbsolutePath();
315
316    LDIFImportConfig importConfig =
317         new LDIFImportConfig(inputPath);
318    importConfig.setInvokeImportPlugins(false);
319    importConfig.setValidateSchema(true);
320
321    String outputPath = inputPath + ".applied." + TimeThread.getGMTTime();
322    if (new File(outputPath).exists())
323    {
324      int i=2;
325      while (true)
326      {
327        if (! new File(outputPath + "." + i).exists())
328        {
329          outputPath = outputPath + "." + i;
330          break;
331        }
332
333        i++;
334      }
335    }
336
337    LDIFExportConfig exportConfig =
338         new LDIFExportConfig(outputPath, ExistingFileBehavior.APPEND);
339    if (logger.isTraceEnabled())
340    {
341      logger.trace("Creating applied file " + outputPath);
342    }
343
344
345    LDIFReader reader = null;
346    LDIFWriter writer = null;
347
348    try
349    {
350      reader = new LDIFReader(importConfig);
351      writer = new LDIFWriter(exportConfig);
352
353      while (true)
354      {
355        ChangeRecordEntry changeRecord;
356        try
357        {
358          changeRecord = reader.readChangeRecord(false);
359          if (logger.isTraceEnabled())
360          {
361            logger.trace("Read change record entry %s", changeRecord);
362          }
363        }
364        catch (LDIFException le)
365        {
366          logger.traceException(le);
367
368          errorEncountered = true;
369          if (le.canContinueReading())
370          {
371            LocalizableMessage m =
372                 ERR_LDIF_CONNHANDLER_CANNOT_READ_CHANGE_RECORD_NONFATAL.get(
373                      le.getMessageObject());
374            writer.writeComment(m, 78);
375            continue;
376          }
377          else
378          {
379            LocalizableMessage m =
380                 ERR_LDIF_CONNHANDLER_CANNOT_READ_CHANGE_RECORD_FATAL.get(
381                      le.getMessageObject());
382            writer.writeComment(m, 78);
383            DirectoryConfig.sendAlertNotification(this,
384                                 ALERT_TYPE_LDIF_CONNHANDLER_PARSE_ERROR, m);
385            break;
386          }
387        }
388
389        Operation operation = null;
390        if (changeRecord == null)
391        {
392          fullyProcessed = true;
393          break;
394        }
395
396        if (changeRecord instanceof AddChangeRecordEntry)
397        {
398          operation = conn.processAdd((AddChangeRecordEntry) changeRecord);
399        }
400        else if (changeRecord instanceof DeleteChangeRecordEntry)
401        {
402          operation = conn.processDelete(
403               (DeleteChangeRecordEntry) changeRecord);
404        }
405        else if (changeRecord instanceof ModifyChangeRecordEntry)
406        {
407          operation = conn.processModify(
408               (ModifyChangeRecordEntry) changeRecord);
409        }
410        else if (changeRecord instanceof ModifyDNChangeRecordEntry)
411        {
412          operation = conn.processModifyDN(
413               (ModifyDNChangeRecordEntry) changeRecord);
414        }
415
416        if (operation == null)
417        {
418          LocalizableMessage m = INFO_LDIF_CONNHANDLER_UNKNOWN_CHANGETYPE.get(
419               changeRecord.getChangeOperationType().getLDIFChangeType());
420          writer.writeComment(m, 78);
421        }
422        else
423        {
424          if (logger.isTraceEnabled())
425          {
426            logger.trace("Result Code: %s", operation.getResultCode());
427          }
428
429          LocalizableMessage m = INFO_LDIF_CONNHANDLER_RESULT_CODE.get(
430                           operation.getResultCode().intValue(),
431                           operation.getResultCode());
432          writer.writeComment(m, 78);
433
434          LocalizableMessageBuilder errorMessage = operation.getErrorMessage();
435          if (errorMessage != null && errorMessage.length() > 0)
436          {
437            m = INFO_LDIF_CONNHANDLER_ERROR_MESSAGE.get(errorMessage);
438            writer.writeComment(m, 78);
439          }
440
441          DN matchedDN = operation.getMatchedDN();
442          if (matchedDN != null)
443          {
444            m = INFO_LDIF_CONNHANDLER_MATCHED_DN.get(matchedDN);
445            writer.writeComment(m, 78);
446          }
447
448          List<String> referralURLs = operation.getReferralURLs();
449          if (referralURLs != null && !referralURLs.isEmpty())
450          {
451            for (String url : referralURLs)
452            {
453              m = INFO_LDIF_CONNHANDLER_REFERRAL_URL.get(url);
454              writer.writeComment(m, 78);
455            }
456          }
457        }
458
459        writer.writeChangeRecord(changeRecord);
460      }
461    }
462    catch (IOException ioe)
463    {
464      logger.traceException(ioe);
465
466      fullyProcessed = false;
467      LocalizableMessage m = ERR_LDIF_CONNHANDLER_IO_ERROR.get(inputPath,
468                                                    getExceptionMessage(ioe));
469      logger.error(m);
470      DirectoryConfig.sendAlertNotification(this,
471                           ALERT_TYPE_LDIF_CONNHANDLER_PARSE_ERROR, m);
472    }
473    finally
474    {
475      close(reader, writer);
476    }
477
478    if (errorEncountered || !fullyProcessed)
479    {
480      String renamedPath = inputPath + ".errors-encountered." +
481                           TimeThread.getGMTTime();
482      if (new File(renamedPath).exists())
483      {
484        int i=2;
485        while (true)
486        {
487          if (! new File(renamedPath + "." + i).exists())
488          {
489            renamedPath = renamedPath + "." + i;
490          }
491
492          i++;
493        }
494      }
495
496      try
497      {
498        if (logger.isTraceEnabled())
499        {
500          logger.trace("Renaming source file to " + renamedPath);
501        }
502
503        ldifFile.renameTo(new File(renamedPath));
504      }
505      catch (Exception e)
506      {
507        logger.traceException(e);
508
509        LocalizableMessage m = ERR_LDIF_CONNHANDLER_CANNOT_RENAME.get(inputPath,
510                         renamedPath, getExceptionMessage(e));
511        logger.error(m);
512        DirectoryConfig.sendAlertNotification(this,
513                             ALERT_TYPE_LDIF_CONNHANDLER_IO_ERROR, m);
514      }
515    }
516    else
517    {
518      try
519      {
520        if (logger.isTraceEnabled())
521        {
522          logger.trace("Deleting source file");
523        }
524
525        ldifFile.delete();
526      }
527      catch (Exception e)
528      {
529        logger.traceException(e);
530
531        LocalizableMessage m = ERR_LDIF_CONNHANDLER_CANNOT_DELETE.get(inputPath,
532                         getExceptionMessage(e));
533        logger.error(m);
534        DirectoryConfig.sendAlertNotification(this,
535                             ALERT_TYPE_LDIF_CONNHANDLER_IO_ERROR, m);
536      }
537    }
538  }
539
540
541
542  /** {@inheritDoc} */
543  @Override
544  public void toString(StringBuilder buffer)
545  {
546    buffer.append("LDIFConnectionHandler(ldifDirectory=\"");
547    buffer.append(ldifDirectory.getAbsolutePath());
548    buffer.append("\", pollInterval=");
549    buffer.append(currentConfig.getPollInterval());
550    buffer.append("ms)");
551  }
552
553
554
555  /** {@inheritDoc} */
556  @Override
557  public boolean isConfigurationAcceptable(ConnectionHandlerCfg configuration,
558                                           List<LocalizableMessage> unacceptableReasons)
559  {
560    LDIFConnectionHandlerCfg cfg = (LDIFConnectionHandlerCfg) configuration;
561    return isConfigurationChangeAcceptable(cfg, unacceptableReasons);
562  }
563
564
565
566  /** {@inheritDoc} */
567  public boolean isConfigurationChangeAcceptable(
568                      LDIFConnectionHandlerCfg configuration,
569                      List<LocalizableMessage> unacceptableReasons)
570  {
571    // The configuration should always be acceptable.
572    return true;
573  }
574
575
576
577  /** {@inheritDoc} */
578  public ConfigChangeResult applyConfigurationChange(
579                                 LDIFConnectionHandlerCfg configuration)
580  {
581    // The only processing we need to do here is to get the LDIF directory and
582    // create a File object from it.
583    File newLDIFDirectory = new File(configuration.getLDIFDirectory());
584    this.ldifDirectory = newLDIFDirectory;
585    currentConfig = configuration;
586    return new ConfigChangeResult();
587  }
588
589
590
591  /** {@inheritDoc} */
592  @Override
593  public DN getComponentEntryDN()
594  {
595    return currentConfig.dn();
596  }
597
598
599
600  /** {@inheritDoc} */
601  public String getClassName()
602  {
603    return LDIFConnectionHandler.class.getName();
604  }
605
606
607
608  /** {@inheritDoc} */
609  public LinkedHashMap<String,String> getAlerts()
610  {
611    LinkedHashMap<String,String> alerts = new LinkedHashMap<>();
612
613    alerts.put(ALERT_TYPE_LDIF_CONNHANDLER_PARSE_ERROR,
614               ALERT_DESCRIPTION_LDIF_CONNHANDLER_PARSE_ERROR);
615    alerts.put(ALERT_TYPE_LDIF_CONNHANDLER_IO_ERROR,
616               ALERT_DESCRIPTION_LDIF_CONNHANDLER_IO_ERROR);
617
618    return alerts;
619  }
620}
621