1   /*
2    * RDFpro - An extensible tool for building stream-oriented RDF processing libraries.
3    * 
4    * Written in 2014 by Francesco Corcoglioniti with support by Marco Amadori, Michele Mostarda,
5    * Alessio Palmero Aprosio and Marco Rospocher. Contact info on http://rdfpro.fbk.eu/
6    * 
7    * To the extent possible under law, the authors have dedicated all copyright and related and
8    * neighboring rights to this software to the public domain worldwide. This software is
9    * distributed without any warranty.
10   * 
11   * You should have received a copy of the CC0 Public Domain Dedication along with this software.
12   * If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
13   */
14  package eu.fbk.rdfpro.tool;
15  
16  import java.io.FilterOutputStream;
17  import java.io.IOException;
18  import java.io.OutputStream;
19  import java.io.PrintStream;
20  
21  import javax.annotation.Nullable;
22  
23  import org.slf4j.Logger;
24  
25  import ch.qos.logback.classic.Level;
26  import ch.qos.logback.classic.spi.ILoggingEvent;
27  import ch.qos.logback.core.UnsynchronizedAppenderBase;
28  import ch.qos.logback.core.encoder.Encoder;
29  import ch.qos.logback.core.pattern.CompositeConverter;
30  import ch.qos.logback.core.pattern.color.ANSIConstants;
31  import ch.qos.logback.core.spi.DeferredProcessingAware;
32  import ch.qos.logback.core.status.ErrorStatus;
33  
34  public final class Logging {
35  
36      private static final boolean ANSI_ENABLED = "true".equalsIgnoreCase(System
37              .getenv("RDFPRO_ANSI_ENABLED"));
38  
39      private static final String SET_DEFAULT_COLOR = ANSIConstants.ESC_START + "0;"
40              + ANSIConstants.DEFAULT_FG + ANSIConstants.ESC_END;
41  
42      private static String format(final String text, @Nullable final String ansiCode) {
43          if (ansiCode == null) {
44              return text;
45          } else {
46              final StringBuilder builder = new StringBuilder(text.length() + 16);
47              builder.append(ANSIConstants.ESC_START);
48              builder.append(ansiCode);
49              builder.append(ANSIConstants.ESC_END);
50              builder.append(text);
51              builder.append(SET_DEFAULT_COLOR);
52              return builder.toString();
53          }
54      }
55  
56      public static void setLevel(final Logger logger, final String level) {
57          final Level l = Level.valueOf(level);
58          ((ch.qos.logback.classic.Logger) logger).setLevel(l);
59      }
60  
61      public static final class NormalConverter extends CompositeConverter<ILoggingEvent> {
62  
63          @Override
64          protected String transform(final ILoggingEvent event, final String in) {
65              if (ANSI_ENABLED) {
66                  final int levelCode = event.getLevel().toInt();
67                  if (levelCode == ch.qos.logback.classic.Level.ERROR_INT) {
68                      return format(in, ANSIConstants.RED_FG);
69                  } else if (levelCode == ch.qos.logback.classic.Level.WARN_INT) {
70                      return format(in, ANSIConstants.MAGENTA_FG);
71                  }
72              }
73              return format(in, null);
74          }
75  
76      }
77  
78      public static final class BoldConverter extends CompositeConverter<ILoggingEvent> {
79  
80          @Override
81          protected String transform(final ILoggingEvent event, final String in) {
82              if (ANSI_ENABLED) {
83                  final int levelCode = event.getLevel().toInt();
84                  if (levelCode == ch.qos.logback.classic.Level.ERROR_INT) {
85                      return format(in, ANSIConstants.BOLD + ANSIConstants.RED_FG);
86                  } else if (levelCode == ch.qos.logback.classic.Level.WARN_INT) {
87                      return format(in, ANSIConstants.BOLD + ANSIConstants.MAGENTA_FG);
88                  } else {
89                      return format(in, ANSIConstants.BOLD + ANSIConstants.DEFAULT_FG);
90                  }
91              }
92              return format(in, null);
93          }
94  
95      }
96  
97      public static final class StatusAppender<E> extends UnsynchronizedAppenderBase<E> {
98  
99          private static final int MAX_STATUS_LENGTH = 256;
100 
101         private Encoder<E> encoder;
102 
103         public synchronized Encoder<E> getEncoder() {
104             return this.encoder;
105         }
106 
107         public synchronized void setEncoder(final Encoder<E> encoder) {
108             if (isStarted()) {
109                 addStatus(new ErrorStatus("Cannot configure appender named \"" + this.name
110                         + "\" after it has been started.", this));
111             }
112             this.encoder = encoder;
113         }
114 
115         @Override
116         public synchronized void start() {
117 
118             // Abort if already started
119             if (this.started) {
120                 return;
121             }
122 
123             // Abort with error if there is no encoder attached to the appender
124             if (this.encoder == null) {
125                 addStatus(new ErrorStatus("No encoder set for the appender named \"" + this.name
126                         + "\".", this));
127                 return;
128             }
129 
130             // Abort if there is no console attached to the process or cannot enable on Windows
131             if (System.console() == null || !ANSI_ENABLED) {
132                 return;
133             }
134 
135             // Setup streams required for generating and displaying status information
136             final PrintStream out = System.out;
137             final StatusAcceptorStream acceptor = new StatusAcceptorStream(out);
138             final OutputStream generator = new StatusGeneratorStream(acceptor);
139 
140             try {
141                 // Setup encoder. On success, replace System.out and start the appender
142                 this.encoder.init(generator);
143                 System.setOut(new PrintStream(acceptor));
144                 super.start();
145             } catch (final IOException ex) {
146                 addStatus(new ErrorStatus("Failed to initialize encoder for appender named \""
147                         + this.name + "\".", this, ex));
148             }
149         }
150 
151         @Override
152         public synchronized void stop() {
153             if (!isStarted()) {
154                 return;
155             }
156             try {
157                 this.encoder.close();
158                 // no need to restore System.out (due to buffering, better not to do that)
159 
160             } catch (final IOException ex) {
161                 addStatus(new ErrorStatus("Failed to write footer for appender named \""
162                         + this.name + "\".", this, ex));
163             } finally {
164                 super.stop();
165             }
166         }
167 
168         @Override
169         protected synchronized void append(final E event) {
170             if (!isStarted()) {
171                 return;
172             }
173             try {
174                 if (event instanceof DeferredProcessingAware) {
175                     ((DeferredProcessingAware) event).prepareForDeferredProcessing();
176                 }
177                 this.encoder.doEncode(event);
178             } catch (final IOException ex) {
179                 stop();
180                 addStatus(new ErrorStatus("IO failure in appender named \"" + this.name + "\".",
181                         this, ex));
182             }
183         }
184 
185         private static final class StatusAcceptorStream extends FilterOutputStream {
186 
187             private static final int ESC = 27;
188 
189             private byte[] status;
190 
191             private boolean statusEnabled;
192 
193             public StatusAcceptorStream(final OutputStream stream) {
194                 super(stream);
195                 this.status = null;
196                 this.statusEnabled = true;
197             }
198 
199             @Override
200             public synchronized void write(final int b) throws IOException {
201                 enableStatus(false);
202                 this.out.write(b);
203                 enableStatus(b == '\n');
204             }
205 
206             @Override
207             public synchronized void write(final byte[] b) throws IOException {
208                 enableStatus(false);
209                 super.write(b);
210                 enableStatus(b[b.length - 1] == '\n');
211             }
212 
213             @Override
214             public synchronized void write(final byte[] b, final int off, final int len)
215                     throws IOException {
216                 enableStatus(false);
217                 super.write(b, off, len);
218                 enableStatus(len > 0 && b[off + len - 1] == '\n');
219             }
220 
221             synchronized void setStatus(final byte[] status) {
222                 final boolean oldEnabled = this.statusEnabled;
223                 enableStatus(false);
224                 this.status = status;
225                 enableStatus(oldEnabled);
226             }
227 
228             private void enableStatus(final boolean enabled) {
229                 try {
230                     if (enabled == this.statusEnabled) {
231                         return;
232                     }
233                     this.statusEnabled = enabled;
234                     if (this.status == null) {
235                         return;
236                     } else if (enabled) {
237                         final int length = Math.min(this.status.length, MAX_STATUS_LENGTH);
238                         this.out.write(this.status, 0, length);
239                         this.out.write('\n'); // move cursor out of the way and cause flush
240                     } else {
241                         final int length = Math.min(this.status.length, MAX_STATUS_LENGTH);
242                         int newlines = 1;
243                         for (int i = 0; i < length; ++i) {
244                             if (this.status[i] == '\n') {
245                                 ++newlines;
246                             }
247                         }
248                         // move cursor up of # lines previously written
249                         this.out.write(ESC);
250                         this.out.write('[');
251                         this.out.write(Integer.toString(newlines).getBytes());
252                         this.out.write('A');
253                         // we emit a newline to move cursor down one line and to column 1, then we
254                         // move up one line, being sure to end up in column 1
255                         this.out.write('\n');
256                         this.out.write(ESC);
257                         this.out.write('[');
258                         this.out.write('1');
259                         this.out.write('A');
260                         // discard everything after the cursor; due to trick above we also discard
261                         // text entered by the user (but not newline - they can be managed by
262                         // saving and restoring cursor position, but many terminals do not handle
263                         // these calls)
264                         this.out.write(ESC);
265                         this.out.write('[');
266                         this.out.write('0');
267                         this.out.write('J');
268                     }
269                 } catch (final Throwable ex) {
270                     if (ex instanceof Error) {
271                         throw (Error) ex;
272                     } else if (ex instanceof RuntimeException) {
273                         throw (RuntimeException) ex;
274                     } else {
275                         throw new RuntimeException(ex);
276                     }
277                 }
278             }
279         }
280 
281         private static final class StatusGeneratorStream extends OutputStream {
282 
283             private final StatusAcceptorStream stream;
284 
285             private final byte[] buffer;
286 
287             private int offset;
288 
289             public StatusGeneratorStream(final StatusAcceptorStream stream) {
290                 this.stream = stream;
291                 this.buffer = new byte[MAX_STATUS_LENGTH];
292                 this.offset = 0;
293             }
294 
295             @Override
296             public void write(final int b) throws IOException {
297                 int emitCount = -1;
298                 if (b == '\n') {
299                     if (this.offset < MAX_STATUS_LENGTH) {
300                         emitCount = this.offset;
301                     }
302                     this.offset = 0;
303                 } else if (this.offset < MAX_STATUS_LENGTH) {
304                     this.buffer[this.offset++] = (byte) b;
305                     if (this.offset == MAX_STATUS_LENGTH) {
306                         emitCount = this.offset;
307                     }
308                 }
309                 if (emitCount >= 0) {
310                     final byte[] status = new byte[emitCount];
311                     System.arraycopy(this.buffer, 0, status, 0, emitCount);
312                     this.stream.setStatus(status);
313                 }
314             }
315 
316             @Override
317             public void write(final byte[] b) throws IOException {
318                 for (int i = 0; i < b.length; ++i) {
319                     write(b[i]);
320                 }
321             }
322 
323             @Override
324             public void write(final byte[] b, final int off, final int len) throws IOException {
325                 final int to = off + len;
326                 for (int i = off; i < to; ++i) {
327                     write(b[i]);
328                 }
329             }
330 
331         }
332 
333     }
334 
335 }