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