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