// Copyright (c) 2012 Jeff Ichnowski
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the OWASP nor the names of its
//       contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.
package org.owasp.encoder;

import java.nio.CharBuffer;
import java.nio.charset.CoderResult;

/**
 *

 * HTMLEncoder -- an encoder for HTML contexts. Currently most HTML-based
 * contexts are properly handled by {@link XMLEncoder}. The remaining
 * HTML-specific context of "unquoted attributes" could not be added to the
 * XMLEncoder without slowing it down. This class implements that remaining
 * context: unquoted attribute values.

 *
 *

 * Note: because this context is likely small strings, and hopefully rarely
 * used, no effort was put into optimizing this encoder.

 *
 * @author Jeff Ichnowski
 */
class HTMLEncoder extends Encoder {

    /**
     * Number of characters in the encoding prefix and suffix when using decimal
     * numeric encodings of the form "&#...;".
     */
    private static final int ENCODE_AFFIX_CHAR_COUNT = 3;

    /**
     * Encoding for '\t'.
     */
    private static final char[] TAB = "	".toCharArray();
    /**
     * Encoding for '&'.
     */
    private static final char[] AMP = "&".toCharArray();
    /**
     * Encoding for '<'.
     */
    private static final char[] LT = "<".toCharArray();
    /**
     * Encoding for '>'.
     */
    private static final char[] GT = ">".toCharArray();

    // The large table-switch implementation used here is fast to
    // implement but slower at runtime than tuned-for-expected-input
    // encoders that use selective if/else's.  Look at the results of
    // BenchmarkTest to see the difference.  See note in javadoc as to
    // reasoning.
    // On Core i7 (Sandybridge)
    // Baseline is 371.401009 ns/op
    // Benchmarked Encode.forXml: 324.219992 ns/op (-12.70% on baseline)
    // Benchmarked Encode.forHtmlUnquotedAttribute: 821.583263 ns/op (+121.21% on baseline)
    @Override
    int maxEncodedLength(int n) {
        // if everything is line separators and paragraph separators then
        // we get "⁛"
        return n * (ENCODE_AFFIX_CHAR_COUNT + 4);
    }

    @Override
    int firstEncodedOffset(String input, int off, int len) {
        final int n = off + len;
        for (int i = off; i < n; ++i) {
            final char ch = input.charAt(i);

            switch (ch) {
                case '\t':
                case '\r':
                case '\f':
                case '\n':
                case ' ':
                case Unicode.NEL:
                case '\"':
                case '\'':
                case '/':
                case '=':
                case '`':
                case '&':
                case '<':
                case '>':
                    return i;

                case '!':
                case '#':
                case '$':
                case '%':
                case '(':
                case ')':
                case '*':
                case '+':
                case ',':
                case '-':
                case '.':

                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case ':':
                case ';':
                case '?':
                case '@':

                case 'A':
                case 'B':
                case 'C':
                case 'D':
                case 'E':
                case 'F':
                case 'G':
                case 'H':
                case 'I':
                case 'J':
                case 'K':
                case 'L':
                case 'M':
                case 'N':
                case 'O':
                case 'P':
                case 'Q':
                case 'R':
                case 'S':
                case 'T':
                case 'U':
                case 'V':
                case 'W':
                case 'X':
                case 'Y':
                case 'Z':

                case '[':
                case '\\':
                case ']':
                case '^':
                case '_':

                case 'a':
                case 'b':
                case 'c':
                case 'd':
                case 'e':
                case 'f':
                case 'g':
                case 'h':
                case 'i':
                case 'j':
                case 'k':
                case 'l':
                case 'm':
                case 'n':
                case 'o':
                case 'p':
                case 'q':
                case 'r':
                case 's':
                case 't':
                case 'u':
                case 'v':
                case 'w':
                case 'x':
                case 'y':
                case 'z':

                case '{':
                case '|':
                case '}':
                case '~':
                    break; // valid

                default:

                    if (Character.isHighSurrogate(ch)) {
                        if (i + 1 < n) {
                            if (Character.isLowSurrogate(input.charAt(i + 1))) {
                                int cp = Character.toCodePoint(ch, input.charAt(i + 1));
                                if (Unicode.isNonCharacter(cp)) {
                                    return i;
                                } else {
                                    ++i;
                                }
                                break;
                            }
                        } else {
                            return i;
                        }
                    }

                    if (ch <= Unicode.MAX_C1_CTRL_CHAR
                            || Character.MIN_SURROGATE <= ch && ch <= Character.MAX_SURROGATE
                            || ch > '\ufffd'
                            || ('\ufdd0' <= ch && ch <= '\ufdef')
                            || ch == Unicode.LINE_SEPARATOR || ch == Unicode.PARAGRAPH_SEPARATOR)
                    {
                        return i;
                    }
            }
        }
        return n;
    }

    /**
     * Appends a source array verbatim to the output array. Caller must insure
     * there is enough space in the array for the output.
     *
     * @param src the characters to copy
     * @param out the output buffer
     * @param j the offset where to write in the output buffer
     * @return {@code j + src.length}
     */
    static int append(char[] src, char[] out, int j) {
        System.arraycopy(src, 0, out, j, src.length);
        return j + src.length;
    }

    /**
     * Appends the numerically encoded version of {@code codePoint} to the
     * output buffer. Caller must insure there is enough space for the output.
     *
     * @param codePoint the character to encode
     * @param out the output buffer
     * @param j the offset where to write in the output buffer
     * @return {@code j} + the encoded length.
     */
    static int encode(int codePoint, char[] out, int j) {
        out[j++] = '&';
        out[j++] = '#';
        if (codePoint >= 1000) {
            out[j++] = (char) (codePoint / 1000 % 10 + '0');
        }
        if (codePoint >= 100) {
            out[j++] = (char) (codePoint / 100 % 10 + '0');
        }
        if (codePoint >= 10) {
            out[j++] = (char) (codePoint / 10 % 10 + '0');
        }
        out[j++] = (char) (codePoint % 10 + '0');
        out[j++] = ';';
        return j;
    }

    @Override
    CoderResult encodeArrays(CharBuffer input, CharBuffer output, boolean endOfInput) {
        final char[] in = input.array();
        final char[] out = output.array();
        int i = input.arrayOffset() + input.position();
        final int n = input.arrayOffset() + input.limit();
        int j = output.arrayOffset() + output.position();
        final int m = output.arrayOffset() + output.limit();

        charLoop:
        for (; i < n; ++i) {
            final char ch = in[i];

            // gigantic switch, hopefully compiled to a tableswitch.
            // this approach appears to be slower than the if/else
            // approach used in the other encoders.  Perhaps an artifact
            // of the CPU's branch predictor, or possible additional
            // overhead of range checking, or having the entire table
            // available to the cache.  If time allows, it would
            // interesting to find out.
            switch (ch) {
                case '\t':
                    if (j + TAB.length > m) {
                        return overflow(input, i, output, j);
                    }
                    j = append(TAB, out, j);
                    break;

                case '\r':
                case '\n':
                case '\f':
                case ' ':
                case '\"':
                case '\'':
                case '/':
                case '=':
                case '`':
                    if (ENCODE_AFFIX_CHAR_COUNT + 2 + j > m) {
                        return overflow(input, i, output, j);
                    }
                    j = encode(ch, out, j);
                    break;

                case Unicode.NEL:
                    if (ENCODE_AFFIX_CHAR_COUNT + 3 + j > m) {
                        return overflow(input, i, output, j);
                    }
                    j = encode(ch, out, j);
                    break;

                case '&':
                    if (j + AMP.length > m) {
                        return overflow(input, i, output, j);
                    }
                    j = append(AMP, out, j);
                    break;

                case '<':
                    if (j + LT.length > m) {
                        return overflow(input, i, output, j);
                    }
                    j = append(LT, out, j);
                    break;

                case '>':
                    if (j + GT.length > m) {
                        return overflow(input, i, output, j);
                    }
                    j = append(GT, out, j);
                    break;

                case '!':
                case '#':
                case '$':
                case '%':
                case '(':
                case ')':
                case '*':
                case '+':
                case ',':
                case '-':
                case '.':

                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case ':':
                case ';':
                case '?':
                case '@':

                case 'A':
                case 'B':
                case 'C':
                case 'D':
                case 'E':
                case 'F':
                case 'G':
                case 'H':
                case 'I':
                case 'J':
                case 'K':
                case 'L':
                case 'M':
                case 'N':
                case 'O':
                case 'P':
                case 'Q':
                case 'R':
                case 'S':
                case 'T':
                case 'U':
                case 'V':
                case 'W':
                case 'X':
                case 'Y':
                case 'Z':

                case '[':
                case '\\':
                case ']':
                case '^':
                case '_':

                case 'a':
                case 'b':
                case 'c':
                case 'd':
                case 'e':
                case 'f':
                case 'g':
                case 'h':
                case 'i':
                case 'j':
                case 'k':
                case 'l':
                case 'm':
                case 'n':
                case 'o':
                case 'p':
                case 'q':
                case 'r':
                case 's':
                case 't':
                case 'u':
                case 'v':
                case 'w':
                case 'x':
                case 'y':
                case 'z':
                case '{':
                case '|':
                case '}':
                case '~':
                    if (j >= m) {
                        return overflow(input, i, output, j);
                    }
                    out[j++] = ch;
                    break;
                default:

                    if (Character.isHighSurrogate(ch)) {
                        if (i + 1 < n) {
                            if (Character.isLowSurrogate(in[i + 1])) {
                                int cp = Character.toCodePoint(ch, in[i + 1]);
                                if (Unicode.isNonCharacter(cp)) {
                                    if (j >= m) {
                                        return overflow(input, i, output, j);
                                    }
                                    out[j++] = '-';
                                    ++i;
                                } else {
                                    if (j + 1 >= m) {
                                        return overflow(input, i, output, j);
                                    }
                                    out[j++] = ch;
                                    out[j++] = in[++i];
                                }
                                break;
                            }
                        } else if (!endOfInput) {
                            break charLoop;
                        }
                    }

                    if (j >= m) {
                        return overflow(input, i, output, j);
                    }

                    if (ch <= Unicode.MAX_C1_CTRL_CHAR
                            || Character.MIN_SURROGATE <= ch && ch <= Character.MAX_SURROGATE
                            || ch > '\ufffd'
                            || ('\ufdd0' <= ch && ch <= '\ufdef'))
                    {
                        // invalid
                        out[j++] = '-';
                    } else if (ch == Unicode.LINE_SEPARATOR || ch == Unicode.PARAGRAPH_SEPARATOR) {
                        if (ENCODE_AFFIX_CHAR_COUNT + 4 + j > m) {
                            return overflow(input, i, output, j);
                        }
                        j = encode(ch, out, j);
                    } else {
                        out[j++] = ch;
                    }
            }
        }

        return underflow(input, i, output, j);
    }
}


// Copyright (c) 2012 Jeff Ichnowski
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the OWASP nor the names of its
//       contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.encoder;

import java.nio.CharBuffer;
import java.nio.charset.CoderResult;

/**
 * ChainedEncoder -- An encoder that chains together two encoders in
 * order.  This is included as an example, but not actually exposed or
 * used as it requires an internal buffer making it not thread-safe.
 * Sequences of 3 or more encodings require chaining together chained
 * encoders.
 *
 * @author Jeff Ichnowski
 */
class ChainedEncoder extends Encoder {
    /** The first encoder to apply in sequence. */
    final Encoder _first;
    /** The second encoder to apply in sequence. */
    final Encoder _last;

    /**
     * The buffer used to store the output of the first encoder before
     * sending as input to the second encoder.
     */
    // TODO: because of this buffer, the ChainedEncoder is the only stateful
    // encoder.  This needs to be removed somehow, or state control APIs need
    // to be added (E.g. reset/flush)
    final CharBuffer _buffer = CharBuffer.allocate(1024);

    /**
     * Creates an ChainedEncoder that applies the encoding sequence
     * {@code input --> first --> last --> output}.
     *
     * @param first the first encoder to apply
     * @param last the second/last encoder to apply.
     */
    ChainedEncoder(Encoder first, Encoder last) {
        _first = first;
        _last = last;
    }

    /**
     * Encodes an input string to an output string.
     *
     * @param str the string to encode
     * @return the encoded string.
     */
    public String encode(String str) {
        if (str == null) {
            str = "null";
        }

        int n = str.length();
        int j = _first.firstEncodedOffset(str, 0, n);
        if (j == n) {
            // string is unchanged after encoding with the first encoder
            return Encode.encode(_last, str);
        }

        final int remaining = n - j;
        final int m = j + _first.maxEncodedLength(n);
        CharBuffer input = CharBuffer.allocate(m);
        str.getChars(0, j, input.array(), 0);
        str.getChars(j, n, input.array(), m - remaining);

        input.limit(m).position(m - remaining);
        CharBuffer tmp = input.duplicate();
        tmp.position(j);

        CoderResult cr = _first.encode(input, tmp, true);
        assert cr.isUnderflow() : "maxEncodedLength was incorrect";

        CharBuffer output = CharBuffer.allocate(_last.maxEncodedLength(tmp.position()));
        tmp.flip();

        cr = _last.encode(tmp, output, true);
        assert cr.isUnderflow() : "maxEncodedLength was incorrect";

        return new String(output.array(), 0, output.position());
    }

    @Override
    protected int firstEncodedOffset(String input, int off, int len) {
        int i = _first.firstEncodedOffset(input, off, len);
        return _last.firstEncodedOffset(input, off, i - off);
    }

    @Override
    protected int maxEncodedLength(int n) {
        return _last.maxEncodedLength(
            _first.maxEncodedLength(n));
    }

//    @Override
//    protected int encode(char[] input, int off, int len, char[] buffer, int j, boolean endOfInput) {
//        char[] tmp = new char[_first.maxEncodedLength(len)];
//        int i = _first.encode(input, off, len, tmp, 0, endOfInput);
//        return _last.encode(tmp, 0, i, buffer, j, endOfInput);
//    }

    @Override
    public CoderResult encode(CharBuffer input, CharBuffer output, boolean endOfInput) {
        for (;;) {
            CoderResult cr1 = _first.encode(input, _buffer, endOfInput);
            _buffer.flip();
            CoderResult cr2 = _last.encode(_buffer, output, endOfInput && cr1.isUnderflow());
            _buffer.compact();

            if (cr2.isOverflow()) {
                // no more room in output, even if we still have buffered input.
                return cr2;
            }

            if (cr1.isUnderflow()) {
                // we've consumed everything from input
                return cr2;
            }
        }
    }

    @Override
    protected CoderResult encodeArrays(CharBuffer input, CharBuffer output, boolean endOfInput) {
        return encode(input, output, endOfInput); // TODO: invert.
    }

    @Override
    public String toString() {
        return "["+_first+","+_last+"]";
    }
}


// Copyright (c) 2012 Jeff Ichnowski
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the OWASP nor the names of its
//       contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.
package org.owasp.encoder;

import java.io.IOException;
import java.io.Writer;
import java.nio.CharBuffer;
import java.nio.charset.CoderResult;

/**
 * EncodedWriter -- A writer the encodes all input for a specific context and writes the encoded output to another writer.
 *
 * @author Jeff Ichnowski
 */
public class EncodedWriter extends Writer {

    /**
     * Buffer size to allocate.
     *
     */
    static final int BUFFER_SIZE = 1024;
    /**
     * Buffer to use for handling characters remaining in the input buffer after an encode. The value is set high enough to handle
     * the lookaheads of all the encoders in the package.
     */
    static final int LEFT_OVER_BUFFER = 16;

    /**
     * The wrapped writer.
     */
    private Writer _out;

    /**
     * The encoder used to encode input to the output writer.
     */
    private Encoder _encoder;

    /**
     * Where encoded output is buffered before sending on to the output writer.
     */
    private CharBuffer _buffer = CharBuffer.allocate(BUFFER_SIZE);

    /**
     * Some encoders require more input or an explicit end-of-input flag before they will process the remaining characters of an
     * input buffer. Because the writer API cannot pass this information on to the caller (e.g. by returning how many bytes were
     * actually written), this writer implementation must buffer up the remaining characters between calls. The
     * _hasLeftOver boolean is a flag used to indicate that there are left over characters in the buffer.
     */
    private boolean _hasLeftOver;

    /**
     * See comment on _hasLeftOver. This buffer is created on-demand once. Whether it has anything to flush is determined by the
     * _hasLeftOver flag.
     */
    private CharBuffer _leftOverBuffer;

    /**
     * Creates an EncodedWriter that uses the specified encoder to encode all input before sending it to the wrapped writer.
     *
     * @param out the target for all writes
     * @param encoder the encoder to use
     */
    public EncodedWriter(Writer out, Encoder encoder) {
        super(out);

//      Reduntant null check, super(out) checks for null and throws NPE.
//        if (out == null) {
//            throw new NullPointerException("writer must not be null");
//        }
        if (encoder == null) {
            throw new NullPointerException("encoder must not be null");
        }

        _out = out;

        _encoder = encoder;
    }

    /**
     * Creates an EncodedWriter that uses the specified encoder to encode all input before sending it to the wrapped writer. This
     * method is equivalent to calling:
     *

     *     new EncodedWriter(out, Encoders.forName(contextName));
     *

     *
     * @param out the target for all writes
     * @param contextName the encoding context name.
     * @throws UnsupportedContextException if the contextName is unrecognized or not supported.
     */
    public EncodedWriter(Writer out, String contextName) throws UnsupportedContextException {
        this(out, Encoders.forName(contextName));
    }

    @Override
    public void write(char[] cbuf, int off, int len) throws IOException {
        synchronized (lock) {
            CharBuffer input = CharBuffer.wrap(cbuf);
            input.limit(off + len).position(off);

            flushLeftOver(input);

            for (;;) {
                CoderResult cr = _encoder.encode(input, _buffer, false);

                if (cr.isUnderflow()) {
                    if (input.hasRemaining()) {
                        if (_leftOverBuffer == null) {
                            _leftOverBuffer = CharBuffer.allocate(LEFT_OVER_BUFFER);
                        }
                        _leftOverBuffer.put(input);
                        _hasLeftOver = true;
                    }
                    return;
                }
                if (cr.isOverflow()) {
                    flushBufferToWriter();
                }
            }
        }
    }

    /**
     * Flushes the contents of the buffer to the writer and resets the buffer to make room for more input.
     *
     * @throws IOException thrown by the wrapped output.
     */
    private void flushBufferToWriter() throws IOException {
        _out.write(_buffer.array(), 0, _buffer.position());
        _buffer.clear();
    }

    /**
     * Flushes the left-over buffer. Characters from the input buffer are used to add more data to the _leftOverBuffer in order to
     * make the flush happen.
     *
     * @param input the next input to encode, or null if at end of file.
     * @throws IOException from the underlying writer.
     */
    private void flushLeftOver(CharBuffer input) throws IOException {
        if (!_hasLeftOver) {
            return;
        }

        for (;;) {
            if (input != null && input.hasRemaining()) {
                _leftOverBuffer.put(input.get());
            }

            _leftOverBuffer.flip();
            CoderResult cr = _encoder.encode(_leftOverBuffer, _buffer, input == null);

            if (cr.isUnderflow()) {
                if (_leftOverBuffer.hasRemaining()) {
                    _leftOverBuffer.compact();
                } else {
                    break;
                }
            }
            if (cr.isOverflow()) {
                flushBufferToWriter();
            }
        }

        _hasLeftOver = false;
        _leftOverBuffer.clear();
    }

    @Override
    public void flush() throws IOException {
        synchronized (lock) {
            flushBufferToWriter();
            _out.flush();
        }
    }

    @Override
    public void close() throws IOException {
        synchronized (lock) {
            flushLeftOver(null);
            flushBufferToWriter();
            _out.close();
        }
    }
}


/*
 * Copyright (C) 2015 Piotr Wittchen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.pwittchen.reactivesensors.library;

import io.reactivex.functions.Predicate;

public final class ReactiveSensorFilter {
  /**
   * Predicate, which can be used in filter(...) method from RxJava
   * to filter all events in which sensor value has changed
   *
   * @return BiPredicate<ReactiveSensorEvent, Boolean> predicate indicating if sensor value has
   * changed
   */
  public static Predicate<ReactiveSensorEvent> filterSensorChanged() {
    return new Predicate<ReactiveSensorEvent>() {
      @Override public boolean test(ReactiveSensorEvent event) throws Exception {
        return event.isSensorChanged();
      }
    };
  }

  /**
   * Predicate, which can be used in filter(...) method from RxJava
   * to filter all events in which accuracy value has changed
   *
   * @return BiPredicate<ReactiveSensorEvent, Boolean> predicate indicating if accuracy value has
   * changed
   */
  public static Predicate<ReactiveSensorEvent> filterAccuracyChanged() {

    return new Predicate<ReactiveSensorEvent>() {
      @Override public boolean test(ReactiveSensorEvent event) throws Exception {
        return event.isAccuracyChanged();
      }
    };
  }
}


/*
 * Copyright (C) 2015 Piotr Wittchen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.pwittchen.reactivesensors.library;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.FlowableEmitter;
import io.reactivex.FlowableOnSubscribe;
import io.reactivex.functions.Action;
import java.util.List;
import java.util.Locale;

/**
 * ReactiveSensors is an Android library
 * monitoring hardware and software sensors with RxJava Observables
 */
public final class ReactiveSensors {

  private SensorManager sensorManager;

  private ReactiveSensors() {
  }

  /**
   * Creates ReactiveSensors object
   *
   * @param context context
   */
  public ReactiveSensors(Context context) {
    this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
  }

  /**
   * Gets lists of all sensors available on device
   *
   * @return list of sensors
   */
  public List<Sensor> getSensors() {
    return sensorManager.getSensorList(Sensor.TYPE_ALL);
  }

  /**
   * Checks if device has given sensor available
   *
   * @param sensorType from Sensor class available in Android SDK
   * @return boolean returns true if sensor is available
   */
  public boolean hasSensor(int sensorType) {
    return sensorManager.getDefaultSensor(sensorType) != null;
  }

  /**
   * Returns RxJava Observable, which allows to monitor hardware sensors
   * as a stream of ReactiveSensorEvent object.
   * Sampling period is set to SensorManager.SENSOR_DELAY_NORMAL.
   *
   * @param sensorType sensor type from Sensor class from Android SDK
   * @return RxJava Observable with ReactiveSensorEvent
   */
  public Flowable<ReactiveSensorEvent> observeSensor(int sensorType) {
    return observeSensor(sensorType, SensorManager.SENSOR_DELAY_NORMAL, null);
  }

  /**
   * Returns RxJava Observable, which allows to monitor hardware sensors
   * as a stream of ReactiveSensorEvent object with defined sampling period
   *
   * @param sensorType sensor type from Sensor class from Android SDK
   * @param samplingPeriodInUs sampling period in microseconds,
   * you can use predefined values from SensorManager class with prefix SENSOR_DELAY
   * @return RxJava Observable with ReactiveSensorEvent
   */
  public Flowable<ReactiveSensorEvent> observeSensor(int sensorType, final int samplingPeriodInUs) {
    return observeSensor(sensorType, samplingPeriodInUs, null);
  }

  public Flowable<ReactiveSensorEvent> observeSensor(int sensorType, final int samplingPeriodInUs,
      final Handler handler) {
    return observeSensor(sensorType, samplingPeriodInUs, handler, BackpressureStrategy.BUFFER);
  }

  /**
   * Returns RxJava Observable, which allows to monitor hardware sensors
   * as a stream of ReactiveSensorEvent object with defined sampling period
   *
   * @param sensorType sensor type from Sensor class from Android SDK
   * @param samplingPeriodInUs sampling period in microseconds,
   * @param handler the Handler the sensor events will be delivered to, use default if it is null
   * you can use predefined values from SensorManager class with prefix SENSOR_DELAY
   * @param strategy BackpressureStrategy for RxJava 2 Flowable type
   * @return RxJava Observable with ReactiveSensorEvent
   */
  public Flowable<ReactiveSensorEvent> observeSensor(int sensorType, final int samplingPeriodInUs,
      final Handler handler, final BackpressureStrategy strategy) {

    if (!hasSensor(sensorType)) {
      String format = "Sensor with id = %d is not available on this device";
      String message = String.format(Locale.getDefault(), format, sensorType);
      return Flowable.error(new SensorNotFoundException(message));
    }

    final Sensor sensor = sensorManager.getDefaultSensor(sensorType);
    final SensorEventListenerWrapper wrapper = new SensorEventListenerWrapper();
    final SensorEventListener listener = wrapper.create();

    return Flowable.create(new FlowableOnSubscribe<ReactiveSensorEvent>() {
      @Override public void subscribe(final FlowableEmitter<ReactiveSensorEvent> emitter)
          throws Exception {

        wrapper.setEmitter(emitter);

        if (handler == null) {
          sensorManager.registerListener(listener, sensor, samplingPeriodInUs);
        } else {
          sensorManager.registerListener(listener, sensor, samplingPeriodInUs, handler);
        }
      }
    }, strategy).doOnCancel(new Action() {
      @Override public void run() throws Exception {
        sensorManager.unregisterListener(listener);
      }
    });
  }
}


package com.sgoertzen.sonarbreak.qualitygate;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sgoertzen.sonarbreak.qualitygate.ConditionStatus;
import com.sgoertzen.sonarbreak.qualitygate.Condition;
import com.sgoertzen.sonarbreak.qualitygate.Result;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;

/**
 * Custom JSON deserializer for a Quality Gate Result.
 */
public class ResultDeserializer extends JsonDeserializer {
    @Override
    public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        Result result = new Result();

        JsonNode root = jsonParser.readValueAsTree();
        result.setId(getText(root, "id"));
        result.setKey(getText(root, "key"));
        result.setName(getText(root, "name"));
        result.setVersion(getText(root, "version"));

        JsonNode dateNode = root.get("date");
        if (dateNode == null) {
            throw new IOException("Node named \"date\" not found in JSON response.");
        }
        String dateString = dateNode.asText();
        DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZZ");
        result.setDatetime(formatter.parseDateTime(dateString));

        JsonNode msr = root.get("msr");
        if (msr == null) {
            throw new IOException("Node named \"msr\" not found in JSON response.");
        }
        Iterator<JsonNode> elements = msr.elements();
        if (elements.hasNext()) {
            String qualityGateJson = elements.next().get("data").asText();

            ObjectMapper map2 = new ObjectMapper();
            JsonNode jsonNode = map2.readTree(qualityGateJson);
            String level = getText(jsonNode, "level");
            result.setStatus(ConditionStatus.forValue(level));

            List<Condition> conditions = map2.convertValue(jsonNode.get("conditions"), new TypeReference<List<Condition>>() {
            });
            result.setConditions(conditions);
        }
        return result;
    }

    /**
     * Get the text from child node with the given name.  Returns null if child node does not exist.
     *
     * @param node         Node to search
     * @param propertyName Name of the child node
     * @return The text value of the child node
     */
    private String getText(JsonNode node, String propertyName) {
        JsonNode childNode = node.get(propertyName);
        if (childNode == null) {
            return null;
        }
        return childNode.asText();
    }
}


package com.sgoertzen.sonarbreak;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sgoertzen.sonarbreak.qualitygate.Query;
import com.sgoertzen.sonarbreak.qualitygate.Result;
import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.logging.Log;
import org.joda.time.DateTime;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.List;

/**
 * Execute a query against Sonar to fetch the quality gate status for a build.  This will look for a sonar status that
 * matches the current build number and that we run in the last minute.  The query will wait up to ten minutes for
 * the results to become available on the sonar server.
 */
public class QueryExecutor {

    public static final String SONAR_FORMAT_PATH = "api/resources/index?resource=%s&metrics=quality_gate_details";
    public static final int SONAR_CONNECTION_RETRIES = 10;
    public static final int SONAR_PROCESSING_WAIT_TIME = 10000;  // wait time between sonar checks in milliseconds

    private final URL sonarURL;
    private final int sonarLookBackSeconds;
    private final int waitForProcessingSeconds;
    private final Log log;

    /**
     * Creates a new executor for running queries against sonar.
     *
     * @param sonarServer              Fully qualified URL to the sonar server
     * @param sonarLookBackSeconds     Amount of time to look back into sonar history for the results of this build
     * @param waitForProcessingSeconds Amount of time to wait for sonar to finish processing the job
     * @param log                      Log for logging  @return Results indicating if the build passed the sonar quality gate checks
     * @throws MalformedURLException If the sonar url is invalid
     */
    public QueryExecutor(String sonarServer, int sonarLookBackSeconds, int waitForProcessingSeconds, Log log) throws MalformedURLException {
        this.sonarURL = new URL(sonarServer);
        this.sonarLookBackSeconds = sonarLookBackSeconds;
        this.waitForProcessingSeconds = waitForProcessingSeconds;
        this.log = log;
    }

    /**
     * Execute the given query on the specified sonar server.
     *
     * @param query The query specifying the project and version of the build
     * @return Result fetched from sonar
     * @throws SonarBreakException If there is an issues communicating with sonar
     * @throws IOException         If the url is invalid
     */
    public Result execute(Query query) throws SonarBreakException, IOException {
        URL queryURL = buildURL(sonarURL, query);
        log.debug("Built a sonar query url of: " + queryURL.toString());

        if (!isURLAvailable(sonarURL, SONAR_CONNECTION_RETRIES)) {
            throw new SonarBreakException(String.format("Unable to get a valid response after %d tries", SONAR_CONNECTION_RETRIES));
        }

        return fetchSonarStatusWithRetries(queryURL, query.getVersion());
    }

    /**
     * Creates a url for the specified quality gate query
     *
     * @param sonarURL The sonar server we will be querying
     * @param query    Holds details on the query we want to make
     * @return A URL object representing the query
     * @throws MalformedURLException    If the sonar url is not valid
     * @throws IllegalArgumentException If the sonar key is not valid
     */
    protected static URL buildURL(URL sonarURL, Query query) throws MalformedURLException, IllegalArgumentException {
        if (query.getSonarKey() == null || query.getSonarKey().length() == 0) {
            throw new IllegalArgumentException("No resource specified in the Query");
        }
        String sonarPathWithResource = String.format(SONAR_FORMAT_PATH, query.getSonarKey());
        return new URL(sonarURL, sonarPathWithResource);
    }

    /**
     * Get the status from sonar for the currently executing build.  This waits for sonar to complete its processing
     * before returning the results.
     *
     * @param queryURL The sonar URL to get the results from
     * @param version  The current project version number
     * @return Matching result object for this build
     * @throws IOException
     * @throws SonarBreakException
     */
    private Result fetchSonarStatusWithRetries(URL queryURL, String version) throws IOException, SonarBreakException {
        DateTime oneMinuteAgo = DateTime.now().minusSeconds(sonarLookBackSeconds);
        DateTime waitUntil = DateTime.now().plusSeconds(waitForProcessingSeconds);
        do {
            // If this is the first time the job is running on sonar the URL might not be available.  Return null and wait.
            if (isURLAvailable(queryURL, 1)) {
                Result result = fetchSonarStatus(queryURL);
                if (result.getVersion().equals(version) && result.getDatetime().isAfter(oneMinuteAgo)) {
                    log.debug("Found a sonar job run that matches version and in the correct time frame");
                    return result;
                }
                String message = String.format("Sleeping while waiting for sonar to process job.  Target Version: %s.  " +
                                "Sonar reporting Version: %s.  Looking back until: %s  Last result time: %s", version,
                        result.getVersion(), oneMinuteAgo.toString(), result.getDatetime().toString());
                log.debug(message);
            } else {
                log.debug(String.format("Query url not available yet: %s", queryURL));
            }
            try {
                Thread.sleep(SONAR_PROCESSING_WAIT_TIME);
            } catch (InterruptedException e) {
                // Do nothing
            }
        } while (!waitUntil.isBeforeNow());

        String message = String.format("Timed out while waiting for Sonar.  Waited %d seconds.  This time can be extended " +
                "using the \"waitForProcessingSeconds\" configuration parameter.", waitForProcessingSeconds);
        throw new SonarBreakException(message);
    }

    /**
     * Get the status of a build project from sonar.  This returns the current status that sonar has and does not
     * do any checking to ensure it matches the current project
     *
     * @param queryURL The sonar URL to hit to get the status
     * @return The sonar response include quality gate status
     * @throws IOException
     * @throws SonarBreakException
     */
    private Result fetchSonarStatus(URL queryURL) throws IOException, SonarBreakException {
        InputStream in = null;
        try {
            URLConnection connection = queryURL.openConnection();
            connection.setRequestProperty("Accept", "application/json");
            in = connection.getInputStream();

            String response = IOUtils.toString(in);
            return parseResponse(response);
        } finally {
            IOUtils.closeQuietly(in);
        }
    }

    /**
     * Pings a HTTP URL. This effectively sends a HEAD request and returns true if the response code is in
     * the 200-399 range.
     *
     * @param url        The HTTP URL to be pinged.
     * @param retryCount How many times to check for sonar before giving up
     * @return true if the given HTTP URL has returned response code 200-399 on a HEAD request,
     * otherwise false.
     * @throws IOException If the sonar server is not available
     */
    protected boolean isURLAvailable(URL url, int retryCount) throws IOException {
        boolean serviceFound = false;
        for (int i = 0; i < retryCount; i++) {
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("HEAD");
            int responseCode = connection.getResponseCode();
            if (200 <= responseCode && responseCode <= 399) {
                log.debug(String.format("Got a valid response of %d from %s", responseCode, url));
                serviceFound = true;
                break;
            } else if (i + 1 < retryCount) { // only sleep if not on last iteration
                try {
                    log.debug("Sleeping while waiting for sonar to become available");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    // do nothing
                }
            }
        }
        return serviceFound;
    }

    /**
     * Parses the string response from sonar into POJOs.
     *
     * @param response The json response from the sonar server.
     * @return Object representing the Sonar response
     * @throws SonarBreakException Thrown if the response is not JSON or it does not contain quality gate data.
     */
    protected static Result parseResponse(String response) throws SonarBreakException {
        ObjectMapper mapper = new ObjectMapper();
        final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        mapper.setDateFormat(df);
        List<Result> results;
        try {
            results = mapper.readValue(response, new TypeReference<List<Result>>() {
            });
        } catch (IOException e) {
            throw new SonarBreakException("Unable to parse the json into a List of QualityGateResults.  Json is: " + response, e);
        }
        if (results == null || results.size() != 1) {
            throw new SonarBreakException("Unable to deserialize JSON response: " + response);
        }
        return results.get(0);
    }
}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.pnc.rest.validation.validators;

import org.apache.commons.validator.routines.UrlValidator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
import java.util.stream.Stream;

/**
 * Author: Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
 * Date: 3/12/16
 * Time: 5:01 PM
 */
public class ScmUrlValidator implements ConstraintValidator<ScmUrl, String>{

    @Override
    public void initialize(ScmUrl constraintAnnotation) {}

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return isValid(value);
    }

    /******************************************************
     * STATIC:
     ******************************************************/
    private static final char USERNAME_INDICATOR = '@';
    private static final Pattern USERNAME_PATTERN = Pattern.compile("[\\.\\-_%\\w]+", Pattern.UNICODE_CHARACTER_CLASS);
    private static final UrlValidator validator = new UrlValidator(new String[]{"http", "https", "git", "ssh", "git+ssh"});

    public static boolean isValid(String url) {
        if (url == null || "".equals(url)) {
            return true;
        }

        if (hasNoProtocol(url)) {
            url = prepareDefaultProtocolUrl(url);
        }

        String username = getUsername(url);
        if (username != null) {
            if (!isValidUsername(username)) {
                return false;
            }
            url = url.replaceFirst(username, "");
        }
        return validator.isValid(url);
    }

    private static String prepareDefaultProtocolUrl(String url) {
        url = url.replaceFirst(":", "/");
        url = "ssh://" + url;
        return url;
    }

    private static boolean hasNoProtocol(String url) {
        return !url.contains("://");
    }

    private static boolean isValidUsername(String username) {
        username = username.replaceFirst("" + USERNAME_INDICATOR, "");
        String[] usernameParts = username.split(":");
        return Stream.of(usernameParts)
                .allMatch(p -> USERNAME_PATTERN.matcher(p).matches());
    }

    /**
     * Returns a String with username.
     * '@' character or whatever indicator of it being username is included in the returned value
     *
     * @param url url to extract username from
     * @return username if username is specified in the url, null otherwise
     */
    private static String getUsername(String url) {
        int atIndex = url.lastIndexOf(USERNAME_INDICATOR);
        if (atIndex < 0) {
            return null;
        }
        String username = url.substring(0, atIndex + 1);
        int slashIndex = username.lastIndexOf('/');
        if (slashIndex > 0) {
            username = username.substring(slashIndex + 1);
        }
        return username;
    }
}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.pnc.indyrepositorymanager;

import org.apache.commons.lang.StringUtils;
import org.commonjava.atlas.maven.ident.ref.ArtifactRef;
import org.commonjava.atlas.maven.ident.ref.SimpleArtifactRef;
import org.commonjava.atlas.maven.ident.util.ArtifactPathInfo;
import org.commonjava.atlas.npm.ident.ref.NpmPackageRef;
import org.commonjava.atlas.npm.ident.util.NpmPackagePathInfo;
import org.commonjava.indy.client.core.Indy;
import org.commonjava.indy.client.core.IndyClientException;
import org.commonjava.indy.client.core.module.IndyContentClientModule;
import org.commonjava.indy.folo.client.IndyFoloAdminClientModule;
import org.commonjava.indy.folo.dto.TrackedContentDTO;
import org.commonjava.indy.folo.dto.TrackedContentEntryDTO;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.indy.promote.client.IndyPromoteClientModule;
import org.commonjava.indy.promote.model.GroupPromoteRequest;
import org.commonjava.indy.promote.model.GroupPromoteResult;
import org.commonjava.indy.promote.model.PathsPromoteRequest;
import org.commonjava.indy.promote.model.PathsPromoteResult;
import org.commonjava.indy.promote.model.PromoteRequest;
import org.commonjava.indy.promote.model.ValidationResult;
import org.jboss.pnc.common.json.moduleconfig.IndyRepoDriverModuleConfig.IgnoredPathSuffixes;
import org.jboss.pnc.common.json.moduleconfig.IndyRepoDriverModuleConfig.InternalRepoPatterns;
import org.jboss.pnc.model.Artifact;
import org.jboss.pnc.model.TargetRepository;
import org.jboss.pnc.spi.coordinator.CompletionStatus;
import org.jboss.pnc.spi.repositorymanager.RepositoryManagerException;
import org.jboss.pnc.spi.repositorymanager.RepositoryManagerResult;
import org.jboss.pnc.spi.repositorymanager.model.RepositoryConnectionInfo;
import org.jboss.pnc.spi.repositorymanager.model.RepositorySession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;

import java.io.File;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.pnc.enums.ArtifactQuality;
import org.jboss.pnc.enums.RepositoryType;
import static org.commonjava.indy.model.core.GenericPackageTypeDescriptor.GENERIC_PKG_KEY;
import static org.commonjava.indy.pkg.maven.model.MavenPackageTypeDescriptor.MAVEN_PKG_KEY;
import static org.commonjava.indy.pkg.npm.model.NPMPackageTypeDescriptor.NPM_PKG_KEY;
import static org.jboss.pnc.indyrepositorymanager.IndyRepositoryConstants.SHARED_IMPORTS_ID;

/**
 * {@link RepositorySession} implementation that works with the Maven {@link RepositoryManagerDriver} (which connects to an
 * Indy server instance for repository management). This session contains connection information for rendering Maven
 * settings.xml files and the like, along with the components necessary to extract the artifacts (dependencies, build uploads)
 * for the associated build.
 *
 * Artifact extraction also implies promotion of imported dependencies to a shared-imports Maven repository for safe keeping. In
 * the case of composed (chained) builds, it also implies promotion of the build output to the associated build-set repository
 * group, to expose them for use in successive builds in the chain.
 */
public class IndyRepositorySession implements RepositorySession {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private static final Logger userLog = LoggerFactory.getLogger("org.jboss.pnc._userlog_.build-executor");

    /** PackageType-specific ignored suffixes. */
    private IgnoredPathSuffixes ignoredPathSuffixes;

    private boolean isTempBuild;

    private Indy indy;
    private Indy serviceAccountIndy;
    private final String buildContentId;
    private final String packageType;
    /**
     * PackageType-specific internal repository name patterns.
     */
    private InternalRepoPatterns internalRepoPatterns;

    private final RepositoryConnectionInfo connectionInfo;

    private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    private String buildPromotionTarget;

    public IndyRepositorySession(Indy indy, Indy serviceAccountIndy, String buildContentId, String packageType,
            IndyRepositoryConnectionInfo info, InternalRepoPatterns internalRepoPatterns,
            IgnoredPathSuffixes ignoredPathSuffixes, String buildPromotionTarget, boolean isTempBuild) {
        this.indy = indy;
        this.serviceAccountIndy = serviceAccountIndy;
        this.buildContentId = buildContentId;
        this.packageType = packageType;
        this.internalRepoPatterns = internalRepoPatterns;
        this.ignoredPathSuffixes = ignoredPathSuffixes;
        this.connectionInfo = info;
        this.buildPromotionTarget = buildPromotionTarget;
        this.isTempBuild = isTempBuild; //TODO define based on buildPromotionTarget
    }

    @Override
    public String toString() {
        return "MavenRepositoryConfiguration " + this.hashCode();
    }

    @Override
    public RepositoryType getType() {
        return toRepoType(packageType);
    }

    @Override
    public String getBuildRepositoryId() {
        return buildContentId;
    }

    @Override
    public RepositoryConnectionInfo getConnectionInfo() {
        return connectionInfo;
    }

    /**
     * Retrieve tracking report from repository manager. Add each tracked download to the dependencies of the build result. Add
     * each tracked upload to the built artifacts of the build result. Promote uploaded artifacts to the product-level storage.
     * Finally, clear the tracking report, and delete the hosted repository + group associated with the completed build.
     */
    @Override
    public RepositoryManagerResult extractBuildArtifacts() throws RepositoryManagerException {
        TrackedContentDTO report;
        try {
            IndyFoloAdminClientModule foloAdmin = indy.module(IndyFoloAdminClientModule.class);
            boolean sealed = foloAdmin.sealTrackingRecord(buildContentId);
            if (!sealed) {
                throw new RepositoryManagerException("Failed to seal content-tracking record for: %s.", buildContentId);
            }

            report = foloAdmin.getTrackingReport(buildContentId);
        } catch (IndyClientException e) {
            throw new RepositoryManagerException("Failed to retrieve tracking report for: %s. Reason: %s", e, buildContentId,
                    e.getMessage());
        }
        if (report == null) {
            throw new RepositoryManagerException("Failed to retrieve tracking report for: %s.", buildContentId);
        }

        Comparator<Artifact> comp = (one, two) -> one.getIdentifier().compareTo(two.getIdentifier());

        List<Artifact> uploads = processUploads(report);
        Collections.sort(uploads, comp);

        List<Artifact> downloads = processDownloads(report);
        Collections.sort(downloads, comp);

        try {
            StoreKey key = new StoreKey(packageType, StoreType.group, buildContentId);
            serviceAccountIndy.stores().delete(key, "[Post-Build] Removing build aggregation group: " + buildContentId);
        } catch (IndyClientException e) {
            throw new RepositoryManagerException("Failed to retrieve Indy stores module. Reason: %s", e, e.getMessage());
        }

        Logger logger = LoggerFactory.getLogger(getClass());
        logger.info("Returning built artifacts / dependencies:\nUploads:\n  {}\n\nDownloads:\n  {}\n\n",
                StringUtils.join(uploads, "\n  "), StringUtils.join(downloads, "\n  "));

        String log = "";
        CompletionStatus status = CompletionStatus.SUCCESS;
        try {
            promoteToBuildContentSet(uploads);
        } catch (RepositoryManagerException rme) {
            status = CompletionStatus.FAILED;
            log = rme.getMessage();
            logger.error("Promotion validation error(s): \n" + log);
            userLog.error("Artifact promotion failed. Promotion validation error(s): {}", log);
            // prevent saving artifacts and dependencies to a failed build
            downloads = Collections.emptyList();
            uploads = Collections.emptyList();
        }

        return new IndyRepositoryManagerResult(uploads, downloads, buildContentId, log, status);
    }

    /**
     * Promote all build dependencies NOT ALREADY CAPTURED to the hosted repository holding store for the shared imports and
     * return dependency artifacts meta data.
     *
     * @param report The tracking report that contains info about artifacts downloaded by the build
     * @return List of dependency artifacts meta data
     * @throws RepositoryManagerException In case of a client API transport error or an error during promotion of artifacts
     */
    private List<Artifact> processDownloads(TrackedContentDTO report) throws RepositoryManagerException {
        Logger logger = LoggerFactory.getLogger(getClass());

        IndyContentClientModule content;
        try {
            content = indy.content();
        } catch (IndyClientException e) {
            throw new RepositoryManagerException("Failed to retrieve Indy content module. Reason: %s", e, e.getMessage());
        }

        List<Artifact> deps = new ArrayList<>();

        Set<TrackedContentEntryDTO> downloads = report.getDownloads();
        if (downloads != null) {
            Map<StoreKey, Map<StoreKey, Set<String>>> toPromote = new HashMap<>();

            Map<String, StoreKey> promotionTargets = new HashMap<>();
            for (TrackedContentEntryDTO download : downloads) {
                String path = download.getPath();
                StoreKey source = download.getStoreKey();
                String packageType = source.getPackageType();
                if (ignoreContent(packageType, path)) {
                    logger.debug("Ignoring download (matched in ignored-suffixes): {} (From: {})", download.getPath(), source);
                    continue;
                }

                // If the entry is from a hosted repository (also shared-imports), it shouldn't be auto-promoted.
                // New binary imports will be coming from a remote repository...
                if (isExternalOrigin(source)) {
                    StoreKey target = null;
                    Map<StoreKey, Set<String>> sources = null;
                    Set<String> paths = null;

                    // this has not been captured, so promote it.
                    switch (packageType) {
                        case MAVEN_PKG_KEY:
                        case NPM_PKG_KEY:
                            target = getPromotionTarget(packageType, promotionTargets);
                            sources = toPromote.computeIfAbsent(target, t -> new HashMap<>());
                            paths = sources.computeIfAbsent(source, s -> new HashSet<>());

                            paths.add(download.getPath());
                            if (MAVEN_PKG_KEY.equals(packageType)) {
                                paths.add(download.getPath() + ".md5");
                                paths.add(download.getPath() + ".sha1");
                            }
                            break;

                        case GENERIC_PKG_KEY:
                            String remoteName = source.getName();
                            String hostedName;
                            if (remoteName.startsWith("r-")) {
                                hostedName = "h-" + remoteName.substring(2);
                            } else {
                                logger.warn("Unexpected generic http remote repo name {}. Using it for hosted repo "
                                        + "without change, but it probably doesn't exist.", remoteName);
                                hostedName = remoteName;
                            }
                            target = new StoreKey(source.getPackageType(), StoreType.hosted, hostedName);
                            sources = toPromote.computeIfAbsent(target, t -> new HashMap<>());
                            paths = sources.computeIfAbsent(source, s -> new HashSet<>());

                            paths.add(download.getPath());
                            break;

                        default:
                            // do not promote anything else anywhere
                            break;
                    }
                }

                String identifier = computeIdentifier(download);

                logger.info("Recording download: {}", identifier);

                String originUrl = download.getOriginUrl();
                if (originUrl == null) {
                    // this is from a hosted repository, either shared-imports or a build, or something like that.
                    originUrl = download.getLocalUrl();
                }

                TargetRepository targetRepository = getDownloadsTargetRepository(download, content);

                Artifact.Builder artifactBuilder = Artifact.Builder.newBuilder()
                        .md5(download.getMd5())
                        .sha1(download.getSha1())
                        .sha256(download.getSha256())
                        .size(download.getSize())
                        .deployPath(download.getPath())
                        .originUrl(originUrl)
                        .importDate(Date.from(Instant.now()))
                        .filename(new File(path).getName())
                        .identifier(identifier)
                        .targetRepository(targetRepository);

                Artifact artifact = validateArtifact(artifactBuilder.build());
                deps.add(artifact);
            }

            for (Map.Entry<StoreKey, Map<StoreKey, Set<String>>> targetToSources : toPromote.entrySet()) {
                StoreKey target = targetToSources.getKey();
                for (Map.Entry<StoreKey, Set<String>> sourceToPaths : targetToSources.getValue().entrySet()) {
                    StoreKey source = sourceToPaths.getKey();
                    PathsPromoteRequest req = new PathsPromoteRequest(source, target, sourceToPaths.getValue()).setPurgeSource(false);
                    // set read-only only the generic http proxy hosted repos, not shared-imports
                    doPromoteByPath(req, GENERIC_PKG_KEY.equals(target.getPackageType()));
                }
            }
        }

        return deps;
    }

    private StoreKey getPromotionTarget(String packageType, Map<String, StoreKey> promotionTargets) {
        if (!promotionTargets.containsKey(packageType)) {
            StoreKey storeKey = new StoreKey(packageType, StoreType.hosted, SHARED_IMPORTS_ID);
            promotionTargets.put(packageType, storeKey);
        }
        return promotionTargets.get(packageType);
    }

    private TargetRepository getDownloadsTargetRepository(TrackedContentEntryDTO download, IndyContentClientModule content) throws RepositoryManagerException {
        String identifier;
        String repoPath;
        StoreKey sk = download.getStoreKey();
        RepositoryType repoType = toRepoType(sk.getPackageType());
        if (repoType == RepositoryType.MAVEN || repoType == RepositoryType.NPM) {
                identifier = "indy-" + repoType.name().toLowerCase();
                repoPath = getTargetRepositoryPath(download, content);
        } else if (repoType == RepositoryType.GENERIC_PROXY) {
                identifier = "indy-http:" + sk.getName();
                repoPath = "/not-available/"; //TODO set the path for http cache
        } else {
            throw new RepositoryManagerException("Repository type " + repoType
                    + " is not supported by Indy repo manager driver.");
        }

        return TargetRepository.newBuilder()
                .identifier(identifier)
                .repositoryType(repoType)
                .repositoryPath(repoPath)
                .temporaryRepo(false)
                .build();
    }

    private String getTargetRepositoryPath(TrackedContentEntryDTO download, IndyContentClientModule content) {
        String result;
        StoreKey sk = download.getStoreKey();
        String packageType = sk.getPackageType();
        if (isExternalOrigin(sk)) {
            result = "/api/" + content.contentPath(new StoreKey(packageType, StoreType.hosted, SHARED_IMPORTS_ID));
        } else {
            result = "/api/" + content.contentPath(sk);
        }
        return result;
    }

    private TargetRepository getUploadsTargetRepository(RepositoryType repoType,
            IndyContentClientModule content) throws RepositoryManagerException {
        StoreType storeType = (isTempBuild ? StoreType.group : StoreType.hosted);
        StoreKey storeKey;
        String identifier;
        if (repoType == RepositoryType.MAVEN) {
            storeKey = new StoreKey(MAVEN_PKG_KEY, storeType, buildPromotionTarget);
            identifier = "indy-maven";
        } else if (repoType == RepositoryType.NPM) {
            storeKey = new StoreKey(NPM_PKG_KEY, storeType, buildPromotionTarget);
            identifier = "indy-npm";
        } else {
            throw new RepositoryManagerException("Repository type " + repoType
                    + " is not supported for uploads by Indy repo manager driver.");
        }

        String repoPath = "/api/" + content.contentPath(storeKey);
        return TargetRepository.newBuilder()
                .identifier(identifier)
                .repositoryType(repoType)
                .repositoryPath(repoPath)
                .temporaryRepo(isTempBuild)
                .build();
    }

    private boolean isExternalOrigin(StoreKey storeKey) {
        if (storeKey.getType() == StoreType.hosted) {
            return false;
        } else {
            String repoName = storeKey.getName();
            List<String> patterns;
            switch (storeKey.getPackageType()) {
                case MAVEN_PKG_KEY:
                    patterns = internalRepoPatterns.getMaven();
                    break;
                case NPM_PKG_KEY:
                    patterns = internalRepoPatterns.getNpm();
                    break;
                case GENERIC_PKG_KEY:
                    patterns = internalRepoPatterns.getGeneric();
                    break;
                default:
                    throw new IllegalArgumentException("Package type " + storeKey.getPackageType()
                            + " is not supported by Indy repository manager driver.");
            }

            for (String pattern : patterns) {
//                Logger logger = LoggerFactory.getLogger(getClass());
//                logger.info( "Checking ")
                if (pattern.equals(repoName)) {
                    return false;
                }

                if (repoName.matches(pattern)) {
                    return false;
                }
            }

            return true;
        }
    }

    /**
     * Return output artifacts metadata.
     *
     * @param report The tracking report that contains info about artifacts uploaded (output) from the build
     * @return List of output artifacts meta data
     * @throws RepositoryManagerException In case of a client API transport error or an error during promotion of artifacts
     */
    private List<Artifact> processUploads(TrackedContentDTO report)
            throws RepositoryManagerException {
        Logger logger = LoggerFactory.getLogger(getClass());

        Set<TrackedContentEntryDTO> uploads = report.getUploads();
        if (uploads != null) {
            List<Artifact> builds = new ArrayList<>();

            for (TrackedContentEntryDTO upload : uploads) {
                String path = upload.getPath();
                StoreKey storeKey = upload.getStoreKey();
                if (ignoreContent(storeKey.getPackageType(), path)) {
                    logger.debug("Ignoring upload (matched in ignored-suffixes): {} (From: {})", path, storeKey);
                    continue;
                }

                String identifier = computeIdentifier(upload);

                logger.info("Recording upload: {}", identifier);

                IndyContentClientModule content;
                try {
                    content = indy.content();
                } catch (IndyClientException e) {
                    throw new RepositoryManagerException("Failed to retrieve Indy content module. Reason: %s", e, e.getMessage());
                }

                RepositoryType repoType = toRepoType(storeKey.getPackageType());
                TargetRepository targetRepository = getUploadsTargetRepository(repoType, content);

                ArtifactQuality artifactQuality = getArtifactQuality(isTempBuild);
                Artifact.Builder artifactBuilder = Artifact.Builder.newBuilder()
                        .md5(upload.getMd5())
                        .sha1(upload.getSha1())
                        .sha256(upload.getSha256())
                        .size(upload.getSize())
                        .deployPath(upload.getPath())
                        .filename(new File(path).getName())
                        .identifier(identifier)
                        .targetRepository(targetRepository)
                        .artifactQuality(artifactQuality);

                Artifact artifact = validateArtifact(artifactBuilder.build());
                builds.add(artifact);
            }

            return builds;
        }
        return Collections.emptyList();
    }

    /**
     * Computes identifier string for an artifact. If the download path is valid for a package-type specific artifact it
     * creates the identifier accordingly.
     *
     * @param transfer the download or upload that we want to generate identifier for
     * @return generated identifier
     */
    private String computeIdentifier(final TrackedContentEntryDTO transfer) {
        String identifier = null;

        switch (transfer.getStoreKey().getPackageType()) {
            case MAVEN_PKG_KEY:
                ArtifactPathInfo pathInfo = ArtifactPathInfo.parse(transfer.getPath());
                if (pathInfo != null) {
                    ArtifactRef aref = new SimpleArtifactRef(pathInfo.getProjectId(), pathInfo.getType(), pathInfo.getClassifier());
                    identifier = aref.toString();
                }
                break;

            case NPM_PKG_KEY:
                NpmPackagePathInfo npmPathInfo = NpmPackagePathInfo.parse(transfer.getPath());
                if (npmPathInfo != null) {
                    NpmPackageRef packageRef = new NpmPackageRef(npmPathInfo.getName(), npmPathInfo.getVersion());
                    identifier = packageRef.toString();
                }
                break;

            case GENERIC_PKG_KEY:
                // handle generic downloads along with other invalid download paths for other package types
                break;

            default:
                // do not do anything by default
                logger.warn("Package type {} is not handled by Indy repository session.", transfer.getStoreKey().getPackageType());
                break;
        }

        if (identifier == null) {
            identifier = computeGenericIdentifier(transfer.getOriginUrl(), transfer.getLocalUrl(), transfer.getSha256());
        }

        return identifier;
    }

    /**
     * Compute the identifier string for a generic download, that does not match package type specific files structure.
     * It prefers to use the origin URL if it is not empty. In case it is then it uses local URL, which can never be
     * empty, it is the local file mirror in Indy. After that it attaches the sha256 separated by a pipe.
     *
     * @param originUrl the origin URL of the transfer, it can be null
     * @param localUrl url where the artifact was backed up in Indy
     * @param sha256 the SHA-256 of the transfer
     * @return the generated identifier
     */
    private String computeGenericIdentifier(String originUrl, String localUrl, String sha256) {
        String identifier = originUrl;
        if (identifier == null) {
            // this is from/to a hosted repository, either the build repo or something like that.
            identifier = localUrl;
        }
        identifier += '|' + sha256;
        return identifier;
    }

    /**
     * Check artifact for any validation errors.  If there are constraint violations, then a RepositoryManagerException is thrown.
     * Otherwise the artifact is returned.
     *
     * @param artifact to validate
     * @return the same artifact
     * @throws RepositoryManagerException if there are constraint violations
     */
    private Artifact validateArtifact(Artifact artifact) throws RepositoryManagerException {
        Set<ConstraintViolation<Artifact>> violations = validator.validate(artifact);
        if (!violations.isEmpty()) {
            throw new RepositoryManagerException("Repository manager returned invalid artifact: " + artifact.toString() + " Constraint Violations: %s", violations);
        }
        return artifact;
    }

    /**
     * Promotes a set of artifact paths (or everything, if the path-set is missing) from a particular Indy artifact store to
     * another, and handle the various error conditions that may arise. If the promote call fails, attempt to rollback before
     * throwing an exception.
     *
     * @param req The promotion request to process, which contains source and target store keys, and (optionally) the set of
     *        paths to promote
     * @param setReadonly flag telling if the target repo should be set to readOnly
     * @throws RepositoryManagerException When either the client API throws an exception due to something unexpected in
     *         transport, or if the promotion process results in an error.
     */
    private void doPromoteByPath(PathsPromoteRequest req, boolean setReadonly) throws RepositoryManagerException {
        IndyPromoteClientModule promoter;
        try {
            promoter = serviceAccountIndy.module(IndyPromoteClientModule.class);
        } catch (IndyClientException e) {
            throw new RepositoryManagerException("Failed to retrieve Indy promote client module. Reason: %s", e, e.getMessage());
        }

        try {
            PathsPromoteResult result = promoter.promoteByPath(req);
            if (result.getError() == null) {
                if (setReadonly && !isTempBuild) {
                    HostedRepository hosted = serviceAccountIndy.stores().load(req.getTarget(), HostedRepository.class);
                    hosted.setReadonly(true);
                    try {
                        serviceAccountIndy.stores().update(hosted, "Setting readonly after successful build and promotion.");
                    } catch (IndyClientException ex) {
                        try {
                            promoter.rollbackPathPromote(result);
                        } catch (IndyClientException ex2) {
                            logger.error("Failed to set readonly flag on repo: %s. Reason given was: %s.", ex, req.getTarget(), ex.getMessage());
                            throw new RepositoryManagerException(
                                    "Subsequently also failed to rollback the promotion of paths from %s to %s. Reason given was: %s",
                                    ex2, req.getSource(), req.getTarget(), ex2.getMessage());
                        }
                        throw new RepositoryManagerException("Failed to set readonly flag on repo: %s. Reason given was: %s",
                                ex, req.getTarget(), ex.getMessage());
                    }
                }
            } else {
                String addendum = "";
                try {
                    PathsPromoteResult rollback = promoter.rollbackPathPromote(result);
                    if (rollback.getError() != null) {
                        addendum = "\nROLLBACK WARNING: Promotion rollback also failed! Reason given: " + result.getError();
                    }

                } catch (IndyClientException e) {
                    throw new RepositoryManagerException("Rollback failed for promotion of: %s. Reason: %s", e, req,
                            e.getMessage());
                }

                throw new RepositoryManagerException("Failed to promote: %s. Reason given was: %s%s", req, result.getError(),
                        addendum);
            }
        } catch (IndyClientException e) {
            throw new RepositoryManagerException("Failed to promote: %s. Reason: %s", e, req, e.getMessage());
        }
    }

    /**
     * Promote the build output to the consolidated build repo (using path promotion, where the build
     * repo contents are added to the repo's contents) and marks the build output as readonly.
     *
     * @param uploads artifacts to be promoted
     */
    public void promoteToBuildContentSet(List<Artifact> uploads) throws RepositoryManagerException {
        IndyPromoteClientModule promoter;
        try {
            promoter = serviceAccountIndy.module(IndyPromoteClientModule.class);
        } catch (IndyClientException e) {
            throw new RepositoryManagerException("Failed to retrieve Indy promote client module. Reason: %s", e, e.getMessage());
        }

        StoreKey buildRepoKey = new StoreKey(packageType, StoreType.hosted, buildContentId);
        PromoteRequest<?> request = null;
        try {
            if (isTempBuild) {
                request = new GroupPromoteRequest(buildRepoKey, buildPromotionTarget);
                GroupPromoteRequest gpReq = (GroupPromoteRequest) request;
                GroupPromoteResult result = promoter.promoteToGroup(gpReq);
                if (!result.succeeded()) {
                    String reason = result.getError();
                    if (reason == null) {
                        ValidationResult validations = result.getValidations();
                        if (validations != null) {
                            StringBuilder sb = new StringBuilder();
                            sb.append("One or more validation rules failed in rule-set ").append(validations.getRuleSet()).append(":\n");

                            validations.getValidatorErrors().forEach((rule, error) -> {
                                sb.append("- ").append(rule).append(":\n").append(error).append("\n\n");
                            });

                            reason = sb.toString();
                        }
                    }

                    throw new RepositoryManagerException("Failed to promote: %s to group: %s. Reason given was: %s",
                            request.getSource(), gpReq.getTargetGroup(), reason);
                }
            } else {
                StoreKey buildTarget = new StoreKey(packageType, StoreType.hosted, buildPromotionTarget);
                Set<String> paths = new HashSet<>();
                for (Artifact a : uploads) {
                    paths.add(a.getDeployPath());
                    paths.add(a.getDeployPath() + ".md5");
                    paths.add(a.getDeployPath() + ".sha1");
                }
                request = new PathsPromoteRequest(buildRepoKey, buildTarget, paths);
                PathsPromoteRequest ppReq = (PathsPromoteRequest) request;
                // TODO request.setPurgeSource(true);

                PathsPromoteResult result = promoter.promoteByPath(ppReq);
                if (result.getError() == null) {
                    HostedRepository buildRepo = serviceAccountIndy.stores().load(buildRepoKey, HostedRepository.class);
                    buildRepo.setReadonly(true);
                    try {
                        serviceAccountIndy.stores().update(buildRepo,
                                "Setting readonly after successful build and promotion.");
                    } catch (IndyClientException ex) {
                        logger.error("Failed to set readonly flag on repo: %s. Reason given was: %s."
                                + " But the promotion to consolidated repo %s succeeded.", ex, buildRepoKey,
                                ex.getMessage(), buildPromotionTarget);
                    }
                } else {
                    String reason = result.getError();
                    if (reason == null) {
                        ValidationResult validations = result.getValidations();
                        if (validations != null) {
                            StringBuilder sb = new StringBuilder();
                            sb.append("One or more validation rules failed in rule-set ").append(validations.getRuleSet()).append(":\n");

                            validations.getValidatorErrors().forEach((rule, error) -> {
                                sb.append("- ").append(rule).append(":\n").append(error).append("\n\n");
                            });

                            reason = sb.toString();
                        }
                    }

                    throw new RepositoryManagerException("Failed to promote files from %s to target %s. Reason given was: %s",
                            request.getSource(), ppReq.getTarget(), reason);
                }
            }
        } catch (IndyClientException e) {
            throw new RepositoryManagerException("Failed to promote: %s. Reason: %s", e, request, e.getMessage());
        }
    }

    private boolean ignoreContent(String packageType, String path) {
        List<String> suffixes;
        switch (packageType) {
            case MAVEN_PKG_KEY:
                suffixes = ignoredPathSuffixes.getMavenWithShared();
                break;
            case NPM_PKG_KEY:
                suffixes = ignoredPathSuffixes.getNpmWithShared();
                break;
            case GENERIC_PKG_KEY:
                suffixes = ignoredPathSuffixes.getShared();
                break;
            default:
                throw new IllegalArgumentException("Package type " + packageType
                        + " is not supported by Indy repository manager driver.");
        }

        for (String suffix : suffixes) {
            if (path.endsWith(suffix)) {
                return true;
            }
        }

        return false;
    }

    private RepositoryType toRepoType(String packageType) {
        switch (packageType) {
            case MAVEN_PKG_KEY:
                return RepositoryType.MAVEN;
            case NPM_PKG_KEY:
                return RepositoryType.NPM;
            case GENERIC_PKG_KEY:
                return RepositoryType.GENERIC_PROXY;
            default:
                return RepositoryType.GENERIC_PROXY;
        }
    }

    private ArtifactQuality getArtifactQuality(boolean isTempBuild) {
        if (isTempBuild) {
            return ArtifactQuality.TEMPORARY;
        } else {
            return ArtifactQuality.NEW;
        }
    }

}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.pnc.spi.coordinator;

import lombok.Getter;

import org.jboss.pnc.model.BuildConfigSetRecord;
import org.jboss.pnc.model.BuildConfiguration;
import org.jboss.pnc.model.BuildConfigurationAudited;
import org.jboss.pnc.model.ProductMilestone;
import org.jboss.pnc.model.User;
import org.jboss.pnc.enums.BuildCoordinationStatus;
import org.jboss.pnc.spi.BuildOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
* Created by Matej Lazar on 2014-12-23.
*/
public class BuildTask {

    private static final Logger log = LoggerFactory.getLogger(BuildTask.class);

    private final Integer id;
    private final BuildConfigurationAudited buildConfigurationAudited; //TODO decouple DB entity

    @Getter
    private final BuildOptions buildOptions;

    private final User user;
    private final Date submitTime;

    private final String contentId;

    private Date startTime;
    private Date endTime;

    private BuildCoordinationStatus status = BuildCoordinationStatus.NEW;
    private String statusDescription;

    /**
     * A list of builds waiting for this build to complete.
     */
    private final Set<BuildTask> dependants = new HashSet<>();

    /**
     * The builds which must be completed before this build can start
     */
    private Set<BuildTask> dependencies = new HashSet<>();

    private final BuildSetTask buildSetTask;

    private ProductMilestone productMilestone;

    private boolean hasFailed = false;

    //called when all dependencies are built
    private final Integer buildConfigSetRecordId;

    private BuildTask(BuildConfigurationAudited buildConfigurationAudited,
                      BuildOptions buildOptions,
                      User user,
                      Date submitTime,
                      BuildSetTask buildSetTask,
                      int id,
                      Integer buildConfigSetRecordId,
                      ProductMilestone productMilestone,
                      String contentId) {

        this.id = id;
        this.buildConfigurationAudited = buildConfigurationAudited;
        this.buildOptions = buildOptions;
        this.user = user;
        this.submitTime = submitTime;

        this.buildSetTask = buildSetTask;
        this.buildConfigSetRecordId = buildConfigSetRecordId;
        this.productMilestone = productMilestone;
        this.contentId = contentId;

    }

    public void setStatus(BuildCoordinationStatus status) {
        this.status = status;
        setHasFailed(status.hasFailed());
    }

    public ProductMilestone getProductMilestone() {
        return productMilestone;
    }

    public Set<BuildTask> getDependencies() {
        return dependencies;
    }

    public void addDependency(BuildTask buildTask) {
        if (!dependencies.contains(buildTask)) {
            dependencies.add(buildTask);
            buildTask.addDependant(this);
        }
    }

    public Set<BuildTask> getDependants() {
        return dependants;
    }

    /**
     * @return current status
     */
    public BuildCoordinationStatus getStatus() {
        return status;
    }

    /**
     * @return Description of current status. Eg. WAITING: there is no available executor; FAILED: exceptionMessage
     */
    public String getStatusDescription() {
        return statusDescription;
    }

    public BuildConfigurationAudited getBuildConfigurationAudited() {
        return buildConfigurationAudited;
    }

    /**
     * Check if this build task has a build configuration dependency on the given build task.
     * The search include transitive dependencies.
     *
     * @param buildTask The buildTask with the config to check
     * @return true if this task's build config has a dependency (including transitive) on the build config of the given task, otherwise false
     */
    public boolean hasConfigDependencyOn(BuildTask buildTask) {
        if (buildTask == null || this.equals(buildTask)) {
            return false;
        }

        BuildConfiguration buildConfiguration = buildConfigurationAudited.getBuildConfiguration();
        if (buildConfiguration == null || buildConfiguration.getAllDependencies() == null) {
            return false;
        }

        return buildConfiguration.dependsOn(buildTask.getBuildConfigurationAudited().getBuildConfiguration());
    }

    /**
     * Check if this build task has a direct build configuration dependency on the given build task
     *
     * @param buildTask The buildTask with the config to check
     * @return true if this task's build config has a direct dependency on the build config of the given task, otherwise false
     */
    public boolean hasDirectConfigDependencyOn(BuildTask buildTask) {
        if (buildTask == null || this.equals(buildTask)) {
            return false;
        }

        BuildConfiguration buildConfiguration = buildConfigurationAudited.getBuildConfiguration();
        if (buildConfiguration == null || buildConfiguration.getDependencies() == null) {
            return false;
        }

        return buildConfiguration.getDependencies().contains(buildTask.getBuildConfigurationAudited().getBuildConfiguration());
    }

    public void addDependant(BuildTask buildTask) {
        if (!dependants.contains(buildTask)) {
            dependants.add(buildTask);
            buildTask.addDependency(this);
        }
    }

    /**
     * A build task is equal to another build task if they are using the same
     * build configuration ID and version.
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        BuildTask buildTask = (BuildTask) o;
        return buildConfigurationAudited.equals(buildTask.getBuildConfigurationAudited());
    }

    @Override
    public int hashCode() {
        return buildConfigurationAudited.hashCode();
    }

    public void setStatusDescription(String statusDescription) {
        this.statusDescription = statusDescription;
    }

    public boolean hasFailed(){
        return this.hasFailed;
    }

    void setHasFailed(boolean hasFailed){
       this.hasFailed = hasFailed;
    }

    public int getId() {
        return id;
    }

    public Date getSubmitTime() {
        return submitTime;
    }

    public Date getStartTime() {
        return startTime;
    }

    public void setStartTime(Date startTime) {
        this.startTime = startTime;
    }

    public Date getEndTime() {
        return endTime;
    }

    public void setEndTime(Date endTime) {
        this.endTime = endTime;
    }

    public User getUser() {
        return user;
    }

    public BuildSetTask getBuildSetTask() {
        return buildSetTask;
    }

    /**
     * Check if this build is ready to build, for example if all dependency builds
     * are complete.
     *
     * @return true if already built, false otherwise
     */
    public boolean readyToBuild() {
        for (BuildTask buildTask : dependencies) {
            if(!buildTask.getStatus().isCompleted()) {
                return false;
            }
        }
        return true;
    }

    @Override
    public String toString() {
        return "Build Task id:" + id + ", name: " + buildConfigurationAudited.getName() + ", project name: " + buildConfigurationAudited.getProject().getName() + ", status: " + status;
    }

    public static BuildTask build(BuildConfigurationAudited buildConfigurationAudited,
                                  BuildOptions buildOptions, User user,
                                  int buildTaskId,
                                  BuildSetTask buildSetTask,
                                  Date submitTime,
                                  ProductMilestone productMilestone,
                                  String contentId) {

        Integer buildConfigSetRecordId = null;
        if (buildSetTask != null) {
            buildConfigSetRecordId =
                    buildSetTask.getBuildConfigSetRecord().map(BuildConfigSetRecord::getId).orElse(null);
        }

        return new BuildTask(
                buildConfigurationAudited,
                buildOptions,
                user,
                submitTime,
                buildSetTask,
                buildTaskId,
                buildConfigSetRecordId,
                productMilestone,
                contentId);
    }

    public Integer getBuildConfigSetRecordId() {
        return buildConfigSetRecordId;
    }

    public String getContentId() {
        return contentId;
    }
}