001/*
002 * Copyright 2015-2023 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2015-2023 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2015-2023 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.listener;
037
038
039
040import java.io.File;
041import java.io.FileOutputStream;
042import java.io.IOException;
043import java.io.OutputStream;
044import java.io.PrintStream;
045import java.util.ArrayList;
046import java.util.Date;
047import java.util.List;
048import java.util.concurrent.atomic.AtomicBoolean;
049
050import com.unboundid.ldap.protocol.AbandonRequestProtocolOp;
051import com.unboundid.ldap.protocol.AddRequestProtocolOp;
052import com.unboundid.ldap.protocol.BindRequestProtocolOp;
053import com.unboundid.ldap.protocol.CompareRequestProtocolOp;
054import com.unboundid.ldap.protocol.DeleteRequestProtocolOp;
055import com.unboundid.ldap.protocol.ExtendedRequestProtocolOp;
056import com.unboundid.ldap.protocol.LDAPMessage;
057import com.unboundid.ldap.protocol.ModifyRequestProtocolOp;
058import com.unboundid.ldap.protocol.ModifyDNRequestProtocolOp;
059import com.unboundid.ldap.protocol.SearchRequestProtocolOp;
060import com.unboundid.ldap.protocol.UnbindRequestProtocolOp;
061import com.unboundid.ldap.sdk.AddRequest;
062import com.unboundid.ldap.sdk.BindRequest;
063import com.unboundid.ldap.sdk.CompareRequest;
064import com.unboundid.ldap.sdk.Control;
065import com.unboundid.ldap.sdk.DeleteRequest;
066import com.unboundid.ldap.sdk.ExtendedRequest;
067import com.unboundid.ldap.sdk.LDAPException;
068import com.unboundid.ldap.sdk.ModifyRequest;
069import com.unboundid.ldap.sdk.ModifyDNRequest;
070import com.unboundid.ldap.sdk.SearchRequest;
071import com.unboundid.ldap.sdk.ToCodeArgHelper;
072import com.unboundid.ldap.sdk.ToCodeHelper;
073import com.unboundid.util.NotMutable;
074import com.unboundid.util.NotNull;
075import com.unboundid.util.Nullable;
076import com.unboundid.util.StaticUtils;
077import com.unboundid.util.ThreadSafety;
078import com.unboundid.util.ThreadSafetyLevel;
079
080
081
082/**
083 * This class provides a request handler that may be used to create a log file
084 * with code that may be used to generate the requests received from clients.
085 * It will be also be associated with another request handler that will actually
086 * be used to handle the request.
087 */
088@NotMutable()
089@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
090public final class ToCodeRequestHandler
091       extends LDAPListenerRequestHandler
092{
093  // Indicates whether any messages have been written to the log so far.
094  @NotNull private final AtomicBoolean firstMessage;
095
096  // Indicates whether the output should include code that may be used to
097  // process the request and handle the response.
098  private final boolean includeProcessing;
099
100  // The client connection with which this request handler is associated.
101  @Nullable private final LDAPListenerClientConnection clientConnection;
102
103  // The request handler that actually will be used to process any requests
104  // received.
105  @NotNull private final LDAPListenerRequestHandler requestHandler;
106
107  // The stream to which the generated code will be written.
108  @NotNull private final PrintStream logStream;
109
110  // Thread-local lists used to hold the generated code.
111  @NotNull private final ThreadLocal<List<String>> lineLists;
112
113
114
115  /**
116   * Creates a new LDAP listener request handler that will write a log file with
117   * LDAP SDK code that corresponds to requests received from clients.  The
118   * requests will be forwarded on to another request handler for further
119   * processing.
120   *
121   * @param  outputFilePath     The path to the output file to be which the
122   *                            generated code should be written.  It must not
123   *                            be {@code null}, and the parent directory must
124   *                            exist.  If a file already exists with the
125   *                            specified path, then new generated code will be
126   *                            appended to it.
127   * @param  includeProcessing  Indicates whether the output should include
128   *                            sample code for processing the request and
129   *                            handling the response.
130   * @param  requestHandler     The request handler that will actually be used
131   *                            to process any requests received.  It must not
132   *                            be {@code null}.
133   *
134   * @throws  IOException  If a problem is encountered while opening the
135   *                       output file for writing.
136   */
137  public ToCodeRequestHandler(@NotNull final String outputFilePath,
138              final boolean includeProcessing,
139              @NotNull final LDAPListenerRequestHandler requestHandler)
140         throws IOException
141  {
142    this(new File(outputFilePath), includeProcessing, requestHandler);
143  }
144
145
146
147  /**
148   * Creates a new LDAP listener request handler that will write a log file with
149   * LDAP SDK code that corresponds to requests received from clients.  The
150   * requests will be forwarded on to another request handler for further
151   * processing.
152   *
153   * @param  outputFile         The output file to be which the generated code
154   *                            should be written.  It must not be {@code null},
155   *                            and the parent directory must exist.  If the
156   *                            file already exists, then new generated code
157   *                            will be appended to it.
158   * @param  includeProcessing  Indicates whether the output should include
159   *                            sample code for processing the request and
160   *                            handling the response.
161   * @param  requestHandler     The request handler that will actually be used
162   *                            to process any requests received.  It must not
163   *                            be {@code null}.
164   *
165   * @throws  IOException  If a problem is encountered while opening the
166   *                       output file for writing.
167   */
168  public ToCodeRequestHandler(@NotNull final File outputFile,
169              final boolean includeProcessing,
170              @NotNull final LDAPListenerRequestHandler requestHandler)
171         throws IOException
172  {
173    this(new FileOutputStream(outputFile, true), includeProcessing,
174         requestHandler);
175  }
176
177
178
179  /**
180   * Creates a new LDAP listener request handler that will write a log file with
181   * LDAP SDK code that corresponds to requests received from clients.  The
182   * requests will be forwarded on to another request handler for further
183   * processing.
184   *
185   * @param  outputStream       The output stream to which the generated code
186   *                            will be written.  It must not be {@code null}.
187   * @param  includeProcessing  Indicates whether the output should include
188   *                            sample code for processing the request and
189   *                            handling the response.
190   * @param  requestHandler     The request handler that will actually be used
191   *                            to process any requests received.  It must not
192   *                            be {@code null}.
193   */
194  public ToCodeRequestHandler(@NotNull final OutputStream outputStream,
195              final boolean includeProcessing,
196              @NotNull final LDAPListenerRequestHandler requestHandler)
197  {
198    logStream = new PrintStream(outputStream, true);
199
200    this.includeProcessing = includeProcessing;
201    this.requestHandler    = requestHandler;
202
203    firstMessage     = new AtomicBoolean(true);
204    lineLists        = new ThreadLocal<>();
205    clientConnection = null;
206  }
207
208
209
210  /**
211   * Creates a new to code request handler instance for the provided client
212   * connection.
213   *
214   * @param  parentHandler  The parent handler with which this instance will be
215   *                        associated.
216   * @param  connection     The client connection for this instance.
217   *
218   * @throws  LDAPException  If a problem is encountered while creating a new
219   *                         instance of the downstream request handler.
220   */
221  private ToCodeRequestHandler(
222               @NotNull final ToCodeRequestHandler parentHandler,
223               @NotNull final LDAPListenerClientConnection connection)
224          throws LDAPException
225  {
226    logStream         = parentHandler.logStream;
227    includeProcessing = parentHandler.includeProcessing;
228    requestHandler    = parentHandler.requestHandler.newInstance(connection);
229    firstMessage      = parentHandler.firstMessage;
230    clientConnection  = connection;
231    lineLists         = parentHandler.lineLists;
232  }
233
234
235
236  /**
237   * {@inheritDoc}
238   */
239  @Override()
240  @NotNull()
241  public ToCodeRequestHandler newInstance(
242              @NotNull final LDAPListenerClientConnection connection)
243         throws LDAPException
244  {
245    return new ToCodeRequestHandler(this, connection);
246  }
247
248
249
250  /**
251   * {@inheritDoc}
252   */
253  @Override()
254  public void closeInstance()
255  {
256    // We'll always close the downstream request handler instance.
257    requestHandler.closeInstance();
258
259
260    // We only want to close the log stream if this is the parent instance that
261    // is not associated with any specific connection.
262    if (clientConnection == null)
263    {
264      synchronized (logStream)
265      {
266        logStream.close();
267      }
268    }
269  }
270
271
272
273  /**
274   * {@inheritDoc}
275   */
276  @Override()
277  public void processAbandonRequest(final int messageID,
278                   @NotNull final AbandonRequestProtocolOp request,
279                   @NotNull final List<Control> controls)
280  {
281    // The LDAP SDK doesn't provide an AbandonRequest object.  In order to
282    // process abandon operations, the LDAP SDK requires the client to have
283    // invoked an asynchronous operation in order to get an AsyncRequestID.
284    // Since this uses LDAPConnection.abandon, then that falls  under the
285    // "processing" umbrella.  So we'll only log something if we should include
286    // processing details.
287    if (includeProcessing)
288    {
289      final List<String> lineList = getLineList(messageID);
290
291      final ArrayList<ToCodeArgHelper> args = new ArrayList<>(2);
292      args.add(ToCodeArgHelper.createRaw(
293           "asyncRequestID" + request.getIDToAbandon(), "Async Request ID"));
294      if (! controls.isEmpty())
295      {
296        final Control[] controlArray = new Control[controls.size()];
297        controls.toArray(controlArray);
298        args.add(ToCodeArgHelper.createControlArray(controlArray,
299             "Request Controls"));
300      }
301
302      ToCodeHelper.generateMethodCall(lineList, 0, null, null,
303           "connection.abandon", args);
304
305      writeLines(lineList);
306    }
307
308    requestHandler.processAbandonRequest(messageID, request, controls);
309  }
310
311
312
313  /**
314   * {@inheritDoc}
315   */
316  @Override()
317  @NotNull()
318  public LDAPMessage processAddRequest(final int messageID,
319                          @NotNull final AddRequestProtocolOp request,
320                          @NotNull final List<Control> controls)
321  {
322    final List<String> lineList = getLineList(messageID);
323
324    final String requestID = "conn" + clientConnection.getConnectionID() +
325         "Msg" + messageID + "Add";
326    final AddRequest addRequest =
327         request.toAddRequest(getControlArray(controls));
328    addRequest.toCode(lineList, requestID, 0, includeProcessing);
329    writeLines(lineList);
330
331    return requestHandler.processAddRequest(messageID, request, controls);
332  }
333
334
335
336  /**
337   * {@inheritDoc}
338   */
339  @Override()
340  @NotNull()
341  public LDAPMessage processBindRequest(final int messageID,
342                          @NotNull final BindRequestProtocolOp request,
343                          @NotNull final List<Control> controls)
344  {
345    final List<String> lineList = getLineList(messageID);
346
347    final String requestID = "conn" + clientConnection.getConnectionID() +
348         "Msg" + messageID + "Bind";
349    final BindRequest bindRequest =
350         request.toBindRequest(getControlArray(controls));
351    bindRequest.toCode(lineList, requestID, 0, includeProcessing);
352    writeLines(lineList);
353
354    return requestHandler.processBindRequest(messageID, request, controls);
355  }
356
357
358
359  /**
360   * {@inheritDoc}
361   */
362  @Override()
363  @NotNull()
364  public LDAPMessage processCompareRequest(final int messageID,
365                          @NotNull final CompareRequestProtocolOp request,
366                          @NotNull final List<Control> controls)
367  {
368    final List<String> lineList = getLineList(messageID);
369
370    final String requestID = "conn" + clientConnection.getConnectionID() +
371         "Msg" + messageID + "Compare";
372    final CompareRequest compareRequest =
373         request.toCompareRequest(getControlArray(controls));
374    compareRequest.toCode(lineList, requestID, 0, includeProcessing);
375    writeLines(lineList);
376
377    return requestHandler.processCompareRequest(messageID, request, controls);
378  }
379
380
381
382  /**
383   * {@inheritDoc}
384   */
385  @Override()
386  @NotNull()
387  public LDAPMessage processDeleteRequest(final int messageID,
388                          @NotNull final DeleteRequestProtocolOp request,
389                          @NotNull final List<Control> controls)
390  {
391    final List<String> lineList = getLineList(messageID);
392
393    final String requestID = "conn" + clientConnection.getConnectionID() +
394         "Msg" + messageID + "Delete";
395    final DeleteRequest deleteRequest =
396         request.toDeleteRequest(getControlArray(controls));
397    deleteRequest.toCode(lineList, requestID, 0, includeProcessing);
398    writeLines(lineList);
399
400    return requestHandler.processDeleteRequest(messageID, request, controls);
401  }
402
403
404
405  /**
406   * {@inheritDoc}
407   */
408  @Override()
409  @NotNull()
410  public LDAPMessage processExtendedRequest(final int messageID,
411                          @NotNull final ExtendedRequestProtocolOp request,
412                          @NotNull final List<Control> controls)
413  {
414    final List<String> lineList = getLineList(messageID);
415
416    final String requestID = "conn" + clientConnection.getConnectionID() +
417         "Msg" + messageID + "Extended";
418    final ExtendedRequest extendedRequest =
419         request.toExtendedRequest(getControlArray(controls));
420    extendedRequest.toCode(lineList, requestID, 0, includeProcessing);
421    writeLines(lineList);
422
423    return requestHandler.processExtendedRequest(messageID, request, controls);
424  }
425
426
427
428  /**
429   * {@inheritDoc}
430   */
431  @Override()
432  @NotNull()
433  public LDAPMessage processModifyRequest(final int messageID,
434                          @NotNull final ModifyRequestProtocolOp request,
435                          @NotNull final List<Control> controls)
436  {
437    final List<String> lineList = getLineList(messageID);
438
439    final String requestID = "conn" + clientConnection.getConnectionID() +
440         "Msg" + messageID + "Modify";
441    final ModifyRequest modifyRequest =
442         request.toModifyRequest(getControlArray(controls));
443    modifyRequest.toCode(lineList, requestID, 0, includeProcessing);
444    writeLines(lineList);
445
446    return requestHandler.processModifyRequest(messageID, request, controls);
447  }
448
449
450
451  /**
452   * {@inheritDoc}
453   */
454  @Override()
455  @NotNull()
456  public LDAPMessage processModifyDNRequest(final int messageID,
457                          @NotNull final ModifyDNRequestProtocolOp request,
458                          @NotNull final List<Control> controls)
459  {
460    final List<String> lineList = getLineList(messageID);
461
462    final String requestID = "conn" + clientConnection.getConnectionID() +
463         "Msg" + messageID + "ModifyDN";
464    final ModifyDNRequest modifyDNRequest =
465         request.toModifyDNRequest(getControlArray(controls));
466    modifyDNRequest.toCode(lineList, requestID, 0, includeProcessing);
467    writeLines(lineList);
468
469    return requestHandler.processModifyDNRequest(messageID, request, controls);
470  }
471
472
473
474  /**
475   * {@inheritDoc}
476   */
477  @Override()
478  @NotNull()
479  public LDAPMessage processSearchRequest(final int messageID,
480                          @NotNull final SearchRequestProtocolOp request,
481                          @NotNull final List<Control> controls)
482  {
483    final List<String> lineList = getLineList(messageID);
484
485    final String requestID = "conn" + clientConnection.getConnectionID() +
486         "Msg" + messageID + "Search";
487    final SearchRequest searchRequest =
488         request.toSearchRequest(getControlArray(controls));
489    searchRequest.toCode(lineList, requestID, 0, includeProcessing);
490    writeLines(lineList);
491
492    return requestHandler.processSearchRequest(messageID, request, controls);
493  }
494
495
496
497  /**
498   * {@inheritDoc}
499   */
500  @Override()
501  public void processUnbindRequest(final int messageID,
502                   @NotNull final UnbindRequestProtocolOp request,
503                   @NotNull final List<Control> controls)
504  {
505    // The LDAP SDK doesn't provide an UnbindRequest object, because it is not
506    // possible to separate an unbind request from a connection closure, which
507    // is done by using LDAPConnection.close method.  That falls  under the
508    // "processing" umbrella, so we'll only log something if we should include
509    // processing details.
510    if (includeProcessing)
511    {
512      final List<String> lineList = getLineList(messageID);
513
514      final ArrayList<ToCodeArgHelper> args = new ArrayList<>(1);
515      if (! controls.isEmpty())
516      {
517        final Control[] controlArray = new Control[controls.size()];
518        controls.toArray(controlArray);
519        args.add(ToCodeArgHelper.createControlArray(controlArray,
520             "Request Controls"));
521      }
522
523      ToCodeHelper.generateMethodCall(lineList, 0, null, null,
524           "connection.close", args);
525
526      writeLines(lineList);
527    }
528
529    requestHandler.processUnbindRequest(messageID, request, controls);
530  }
531
532
533
534  /**
535   * Retrieves a list to use to hold the lines of output.  It will include
536   * comments with information about the client that submitted the request.
537   *
538   * @param  messageID  The message ID for the associated request.
539   *
540   * @return  A list to use to hold the lines of output.
541   */
542  @NotNull()
543  private List<String> getLineList(final int messageID)
544  {
545    // Get a thread-local string list, creating it if necessary.
546    List<String> lineList = lineLists.get();
547    if (lineList == null)
548    {
549      lineList = new ArrayList<>(20);
550      lineLists.set(lineList);
551    }
552    else
553    {
554      lineList.clear();
555    }
556
557
558    // Add the appropriate header content to the list.
559    lineList.add("// Time:  " + new Date());
560    lineList.add("// Client Address: " +
561         clientConnection.getSocket().getInetAddress().getHostAddress() + ':' +
562         clientConnection.getSocket().getPort());
563    lineList.add("// Server Address: " +
564         clientConnection.getSocket().getLocalAddress().getHostAddress() + ':' +
565         clientConnection.getSocket().getLocalPort());
566    lineList.add("// Connection ID: " + clientConnection.getConnectionID());
567    lineList.add("// Message ID: " + messageID);
568
569    return lineList;
570  }
571
572
573
574  /**
575   * Writes the lines contained in the provided list to the output stream.
576   *
577   * @param  lineList  The list containing the lines to be written.
578   */
579  private void writeLines(@NotNull final List<String> lineList)
580  {
581    synchronized (logStream)
582    {
583      if (! firstMessage.compareAndSet(true, false))
584      {
585        logStream.println();
586        logStream.println();
587      }
588
589      for (final String s : lineList)
590      {
591        logStream.println(s);
592      }
593    }
594  }
595
596
597
598  /**
599   * Converts the provided list of controls into an array of controls.
600   *
601   * @param  controls  The list of controls to convert to an array.
602   *
603   * @return  An array of controls that corresponds to the provided list.
604   */
605  @NotNull()
606  private static Control[] getControlArray(
607               @Nullable final List<Control> controls)
608  {
609    if ((controls == null) || controls.isEmpty())
610    {
611      return StaticUtils.NO_CONTROLS;
612    }
613
614    final Control[] controlArray = new Control[controls.size()];
615    return controls.toArray(controlArray);
616  }
617}