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