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