package org.apache.lucene.util;

/**
 * A builder for {@link CharsRef} instances.
 *
 * @lucene.internal
 */
public class CharsRefBuilder implements Appendable {

    private static final String NULL_STRING = "null";

    private final CharsRef ref;

    public CharsRefBuilder() {
        ref = new CharsRef();
    }

    /**
     * Return a reference to the chars of this builder.
     */
    public char[] chars() {
        return ref.chars;
    }

    /**
     * Return the number of chars in this buffer.
     */
    public int length() {
        return ref.length;
    }

    /**
     * Set the length.
     */
    public void setLength(int length) {
        this.ref.length = length;
    }

    /**
     * Return the char at the given offset.
     */
    public char charAt(int offset) {
        return ref.chars[offset];
    }

    public void setCharAt(int offset, char b) {
        ref.chars[offset] = b;
    }

    /**
     * Reset this builder to the empty state.
     */
    public void clear() {
        ref.length = 0;
    }

    @Override
    public CharsRefBuilder append(CharSequence csq) {
        if (csq == null) {
            return append(NULL_STRING);
        }
        return append(csq, 0, csq.length());
    }

    @Override
    public CharsRefBuilder append(CharSequence csq, int start, int end) {
        if (csq == null) {
            return append(NULL_STRING);
        }
        grow(ref.length + end - start);
        for (int i = start; i < end; ++i) {
            setCharAt(ref.length++, csq.charAt(i));
        }
        return this;
    }

    @Override
    public CharsRefBuilder append(char c) {
        grow(ref.length + 1);
        setCharAt(ref.length++, c);
        return this;
    }

    /**
     * Copies the given {@link CharsRef} referenced content into this instance.
     */
    public void copyChars(CharsRef other) {
        copyChars(other.chars, other.offset, other.length);
    }

    /**
     * Used to grow the reference array.
     */
    public void grow(int newLength) {
        ref.chars = ArrayUtil.grow(ref.chars, newLength);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(byte[] bytes, int offset, int length) {
        grow(length);
        ref.length = UnicodeUtil.UTF8toUTF16(bytes, offset, length, ref.chars);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(BytesRef bytes) {
        copyUTF8Bytes(bytes.bytes, bytes.offset, bytes.length);
    }

    /**
     * Copies the given array into this instance.
     */
    public void copyChars(char[] otherChars, int otherOffset, int otherLength) {
        grow(otherLength);
        System.arraycopy(otherChars, otherOffset, ref.chars, 0, otherLength);
        ref.length = otherLength;
    }

    /**
     * Appends the given array to this CharsRef
     */
    public void append(char[] otherChars, int otherOffset, int otherLength) {
        int newLen = ref.length + otherLength;
        grow(newLen);
        System.arraycopy(otherChars, otherOffset, ref.chars, ref.length, otherLength);
        ref.length = newLen;
    }

    /**
     * Return a {@link CharsRef} that points to the internal content of this builder. Any update to
     * the content of this builder might invalidate the provided ref and vice-versa.
     */

{
        assert ref.offset == 0 : "Modifying the offset of the returned ref is illegal";
        return ref;
    }

    /**
     * Build a new {@link CharsRef} that has the same content as this builder.
     */
    public CharsRef toCharsRef() {
        return new CharsRef(ArrayUtil.copyOfSubArray(ref.chars, 0, ref.length), 0, ref.length);
    }

    @Override
    public String toString() {
        return get().toString();
    }

    @Override
    public boolean equals(Object obj) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int hashCode() {
        throw new UnsupportedOperationException();
    }
}


{
        if (csq == null) {
            return append(NULL_STRING);
        }
        grow(ref.length + end - start);
        for (int i = start; i < end; ++i) {
            setCharAt(ref.length++, csq.charAt(i));
        }
        return this;
    }

package org.apache.lucene.util;

/**
 * A builder for {@link CharsRef} instances.
 *
 * @lucene.internal
 */
public class CharsRefBuilder implements Appendable {

    private static final String NULL_STRING = "null";

    private final CharsRef ref;

    public CharsRefBuilder() {
        ref = new CharsRef();
    }

    /**
     * Return a reference to the chars of this builder.
     */
    public char[] chars() {
        return ref.chars;
    }

    /**
     * Return the number of chars in this buffer.
     */
    public int length() {
        return ref.length;
    }

    /**
     * Set the length.
     */
    public void setLength(int length) {
        this.ref.length = length;
    }

    /**
     * Return the char at the given offset.
     */
    public char charAt(int offset) {
        return ref.chars[offset];
    }

    public void setCharAt(int offset, char b) {
        ref.chars[offset] = b;
    }

    /**
     * Reset this builder to the empty state.
     */
    public void clear() {
        ref.length = 0;
    }

    @Override
    public CharsRefBuilder append(CharSequence csq) {
        if (csq == null) {
            return append(NULL_STRING);
        }
        return append(csq, 0, csq.length());
    }

    @Override
    public CharsRefBuilder append(CharSequence csq, int start, int end)

@Override
    public CharsRefBuilder append(char c) {
        grow(ref.length + 1);
        setCharAt(ref.length++, c);
        return this;
    }

    /**
     * Copies the given {@link CharsRef} referenced content into this instance.
     */
    public void copyChars(CharsRef other) {
        copyChars(other.chars, other.offset, other.length);
    }

    /**
     * Used to grow the reference array.
     */
    public void grow(int newLength) {
        ref.chars = ArrayUtil.grow(ref.chars, newLength);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(byte[] bytes, int offset, int length) {
        grow(length);
        ref.length = UnicodeUtil.UTF8toUTF16(bytes, offset, length, ref.chars);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(BytesRef bytes) {
        copyUTF8Bytes(bytes.bytes, bytes.offset, bytes.length);
    }

    /**
     * Copies the given array into this instance.
     */
    public void copyChars(char[] otherChars, int otherOffset, int otherLength) {
        grow(otherLength);
        System.arraycopy(otherChars, otherOffset, ref.chars, 0, otherLength);
        ref.length = otherLength;
    }

    /**
     * Appends the given array to this CharsRef
     */
    public void append(char[] otherChars, int otherOffset, int otherLength) {
        int newLen = ref.length + otherLength;
        grow(newLen);
        System.arraycopy(otherChars, otherOffset, ref.chars, ref.length, otherLength);
        ref.length = newLen;
    }

    /**
     * Return a {@link CharsRef} that points to the internal content of this builder. Any update to
     * the content of this builder might invalidate the provided ref and vice-versa.
     */
    public CharsRef get() {
        assert ref.offset == 0 : "Modifying the offset of the returned ref is illegal";
        return ref;
    }

    /**
     * Build a new {@link CharsRef} that has the same content as this builder.
     */
    public CharsRef toCharsRef() {
        return new CharsRef(ArrayUtil.copyOfSubArray(ref.chars, 0, ref.length), 0, ref.length);
    }

    @Override
    public String toString() {
        return get().toString();
    }

    @Override
    public boolean equals(Object obj) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int hashCode() {
        throw new UnsupportedOperationException();
    }
}


package org.apache.lucene.util;

/**
 * A builder for {@link CharsRef} instances.
 *
 * @lucene.internal
 */
public class CharsRefBuilder implements Appendable {

    private static final String NULL_STRING = "null";

    private final CharsRef ref;

    public CharsRefBuilder() {
        ref = new CharsRef();
    }

    /**
     * Return a reference to the chars of this builder.
     */
    public char[] chars() {
        return ref.chars;
    }

    /**
     * Return the number of chars in this buffer.
     */
    public int length() {
        return ref.length;
    }

    /**
     * Set the length.
     */
    public void setLength(int length) {
        this.ref.length = length;
    }

    /**
     * Return the char at the given offset.
     */
    public char charAt(int offset) {
        return ref.chars[offset];
    }

    public void setCharAt(int offset, char b) {
        ref.chars[offset] = b;
    }

    /**
     * Reset this builder to the empty state.
     */
    public void clear() {
        ref.length = 0;
    }

    @Override
    public CharsRefBuilder append(CharSequence csq) {
        if (csq == null) {
            return append(NULL_STRING);
        }
        return append(csq, 0, csq.length());
    }

    @Override
    public CharsRefBuilder append(CharSequence csq, int start, int end) {
        if (csq == null) {
            return append(NULL_STRING);
        }
        grow(ref.length + end - start);
        for (int i = start; i < end; ++i) {
            setCharAt(ref.length++, csq.charAt(i));
        }
        return this;
    }

    @Override
    public CharsRefBuilder append(char c) {
        grow(ref.length + 1);
        setCharAt(ref.length++, c);
        return this;
    }

    /**
     * Copies the given {@link CharsRef} referenced content into this instance.
     */
    public void copyChars(CharsRef other) {
        copyChars(other.chars, other.offset, other.length);
    }

    /**
     * Used to grow the reference array.
     */
    public void grow(int newLength) {
        ref.chars = ArrayUtil.grow(ref.chars, newLength);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(byte[] bytes, int offset, int length) {
        grow(length);
        ref.length = UnicodeUtil.UTF8toUTF16(bytes, offset, length, ref.chars);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(BytesRef bytes) {
        copyUTF8Bytes(bytes.bytes, bytes.offset, bytes.length);
    }

    /**
     * Copies the given array into this instance.
     */
    public void copyChars(char[] otherChars, int otherOffset, int otherLength) {
        grow(o

System.arraycopy(otherChars, otherOffset, ref.chars, 0, otherLength);
        ref.length = otherLength;
    }

    /**
     * Appends the given array to this CharsRef
     */
    public void append(char[] otherChars, int otherOffset, int otherLength) {
        int newLen = ref.length + otherLength;
        grow(newLen);
        System.arraycopy(otherChars, otherOffset, ref.chars, ref.length, otherLength);
        ref.length = newLen;
    }

    /**
     * Return a {@link CharsRef} that points to the internal content of this builder. Any update to
     * the content of this builder might invalidate the provided ref and vice-versa.
     */
    public CharsRef get() {
        assert ref.offset == 0 : "Modifying the offset of the returned ref is illegal";
        return ref;
    }

    /**
     * Build a new {@link CharsRef} that has the same content as this builder.
     */
    public CharsRef toCharsRef() {
        return new CharsRef(ArrayUtil.copyOfSubArray(ref.chars, 0, ref.length), 0, ref.length);
    }

    @Override
    public String toString() {
        return get().toString();
    }

    @Override
    public boolean equals(Object obj) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int hashCode() {
        throw new UnsupportedOperationException();
    }
}


public void append(char[] otherChars, int otherOffset, int otherLength) {
        int newLen = ref.length + otherLength;
        grow(newLen);
        System.arraycopy(otherChars, otherOffset, ref.chars, ref.length, otherLength);
        ref.length = newLen;
    }

package org.apache.lucene.util;

/**
 * A builder for {@link CharsRef} instances.
 *
 * @lucene.internal
 */
public class CharsRefBuilder implements Appendable {

    private static final String NULL_STRING = "null";

    private final CharsRef ref;

    public CharsRefBuilder() {
        ref = new CharsRef();
    }

    /**
     * Return a reference to the chars of this builder.
     */
    public char[] chars() {
        return ref.chars;
    }

    /**
     * Return the number of chars in this buffer.
     */
    public int length() {
        return ref.length;
    }

    /**
     * Set the length.
     */
    public void setLength(int length) {
        this.ref.length = length;
    }

    /**
     * Return the char at the given offset.
     */
    public char charAt(int offset) {
        return ref.chars[offset];
    }

    public void setCharAt(int offset, char b) {
        ref.chars[offset] = b;
    }

    /**
     * Reset this builder to the empty state.
     */
    public void clear() {
        ref.length = 0;
    }

    @Override
    public CharsRefBuilder append(CharSequence csq) {
        if (csq == null) {
            return append(NULL_STRING);
        }
        return append(csq, 0, csq.length());
    }

    @Override
    public CharsRefBuilder append(CharSequence csq, int start, int end) {
        if (csq == null) {
            return append(NULL_STRING);
        }
        grow(ref.length + end - start);
        for (int i = start; i < end; ++i) {
            setCharAt(ref.length++, csq.charAt(i));
        }
        return this;
    }

    @Override
    public CharsRefBuilder append(char c) {
        grow(ref.length + 1);
        setCharAt(ref.length++, c);
        return this;
    }

    /**
     * Copies the given {@link CharsRef} referenced content into this instance.
     */
    public void copyChars(CharsRef other) {
        copyChars(other.chars, other.offset, other.length);
    }

    /**
     * Used to grow the reference array.
     */
    public void grow(int newLength) {
        ref.chars = ArrayUtil.grow(ref.chars, newLength);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(byte[] bytes, int offset, int length) {
        grow(length);
        ref.length = UnicodeUtil.UTF8toUTF16(bytes, offset, length, ref.chars);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(BytesRef bytes) {
        copyUTF8Bytes(bytes.bytes, bytes.offset, bytes.length);
    }

    /**
     * Copies the given array into this instance.
     */
    public void copyChars(char[] otherChars, int otherOffset, int otherLength) {
        grow(otherLength);
        System.arraycopy(otherChars, otherOffset, ref.chars, 0, otherLength);
        ref.length = otherLength;
    }

    /**
     * Appends the given array to this CharsRef
     */

/**
     * Return a {@link CharsRef} that points to the internal content of this builder. Any update to
     * the content of this builder might invalidate the provided ref and vice-versa.
     */
    public CharsRef get() {
        assert ref.offset == 0 : "Modifying the offset of the returned ref is illegal";
        return ref;
    }

    /**
     * Build a new {@link CharsRef} that has the same content as this builder.
     */
    public CharsRef toCharsRef() {
        return new CharsRef(ArrayUtil.copyOfSubArray(ref.chars, 0, ref.length), 0, ref.length);
    }

    @Override
    public String toString() {
        return get().toString();
    }

    @Override
    public boolean equals(Object obj) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int hashCode() {
        throw new UnsupportedOperationException();
    }
}


for (int i = start; i < end; ++i) {
            setCharAt(ref.length++, csq.charAt(i));
        }

package org.apache.lucene.util;

/**
 * A builder for {@link CharsRef} instances.
 *
 * @lucene.internal
 */
public class CharsRefBuilder implements Appendable {

    private static final String NULL_STRING = "null";

    private final CharsRef ref;

    public CharsRefBuilder() {
        ref = new CharsRef();
    }

    /**
     * Return a reference to the chars of this builder.
     */
    public char[] chars() {
        return ref.chars;
    }

    /**
     * Return the number of chars in this buffer.
     */
    public int length() {
        return ref.length;
    }

    /**
     * Set the length.
     */
    public void setLength(int length) {
        this.ref.length = length;
    }

    /**
     * Return the char at the given offset.
     */
    public char charAt(int offset) {
        return ref.chars[offset];
    }

    public void setCharAt(int offset, char b) {
        ref.chars[offset] = b;
    }

    /**
     * Reset this builder to the empty state.
     */
    public void clear() {
        ref.length = 0;
    }

    @Override
    public CharsRefBuilder append(CharSequence csq) {
        if (csq == null) {
            return append(NULL_STRING);
        }
        return append(csq, 0, csq.length());
    }

    @Override
    public CharsRefBuilder append(CharSequence csq, int start, int end) {
        if (csq == null) {
            return append(NULL_STRING);
        }
        grow(ref.length + end - start);

return this;
    }

    @Override
    public CharsRefBuilder append(char c) {
        grow(ref.length + 1);
        setCharAt(ref.length++, c);
        return this;
    }

    /**
     * Copies the given {@link CharsRef} referenced content into this instance.
     */
    public void copyChars(CharsRef other) {
        copyChars(other.chars, other.offset, other.length);
    }

    /**
     * Used to grow the reference array.
     */
    public void grow(int newLength) {
        ref.chars = ArrayUtil.grow(ref.chars, newLength);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(byte[] bytes, int offset, int length) {
        grow(length);
        ref.length = UnicodeUtil.UTF8toUTF16(bytes, offset, length, ref.chars);
    }

    /**
     * Copy the provided bytes, interpreted as UTF-8 bytes.
     */
    public void copyUTF8Bytes(BytesRef bytes) {
        copyUTF8Bytes(bytes.bytes, bytes.offset, bytes.length);
    }

    /**
     * Copies the given array into this instance.
     */
    public void copyChars(char[] otherChars, int otherOffset, int otherLength) {
        grow(otherLength);
        System.arraycopy(otherChars, otherOffset, ref.chars, 0, otherLength);
        ref.length = otherLength;
    }

    /**
     * Appends the given array to this CharsRef
     */
    public void append(char[] otherChars, int otherOffset, int otherLength) {
        int newLen = ref.length + otherLength;
        grow(newLen);
        System.arraycopy(otherChars, otherOffset, ref.chars, ref.length, otherLength);
        ref.length = newLen;
    }

    /**
     * Return a {@link CharsRef} that points to the internal content of this builder. Any update to
     * the content of this builder might invalidate the provided ref and vice-versa.
     */
    public CharsRef get() {
        assert ref.offset == 0 : "Modifying the offset of the returned ref is illegal";
        return ref;
    }

    /**
     * Build a new {@link CharsRef} that has the same content as this builder.
     */
    public CharsRef toCharsRef() {
        return new CharsRef(ArrayUtil.copyOfSubArray(ref.chars, 0, ref.length), 0, ref.length);
    }

    @Override
    public String toString() {
        return get().toString();
    }

    @Override
    public boolean equals(Object obj) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int hashCode() {
        throw new UnsupportedOperationException();
    }
}


package com.vip.saturn.job.executor;

import com.vip.saturn.job.threads.SaturnThreadFactory;
import com.vip.saturn.job.utils.LogEvents;
import com.vip.saturn.job.utils.LogUtils;
import com.vip.saturn.job.utils.SystemEnvProperties;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Regular truncate saturn-nohup.out while the size is over specified limit.
 */
public class PeriodicTruncateNohupOutService {

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

    private static final long TRUNCATE_SIZE = 0;

    private ScheduledExecutorService truncateLogService;

    public PeriodicTruncateNohupOutService(String executorName) {
        truncateLogService = Executors.newScheduledThreadPool(1, new SaturnThreadFactory(executorName + "-truncate-nohup-out-thread", false));
    }

    public void start() {
        LogUtils.info(log, LogEvents.ExecutorEvent.INIT, "start PeriodicTruncateNohupOutService");
        if (StringUtils.isBlank(SystemEnvProperties.VIP_SATURN_LOG_OUTFILE)) {
            LogUtils.warn(log, LogEvents.ExecutorEvent.INIT, "File path of saturn-nohup.out is not set, please check.");
            return;
        }
        truncateLogService.scheduleAtFixedRate(new TruncateLogRunnable(), new Random().nextInt(10), SystemEnvProperties.VIP_SATURN_CHECK_NOHUPOUT_SIZE_INTERVAL_IN_SEC, TimeUnit.SECONDS);
    }

    public void shutdown() {
        LogUtils.info(log, LogEvents.ExecutorEvent.SHUTDOWN, "shutdown PeriodicTruncateNohupOutService");
        if (truncateLogService != null) {
            truncateLogService.shutdownNow();
        }
    }

    private class TruncateLogRunnable implements Runnable {

{
            try (RandomAccessFile file = new RandomAccessFile(SystemEnvProperties.VIP_SATURN_LOG_OUTFILE, "rw");
                FileChannel fc = file.getChannel()) {
                if (fc.size() > SystemEnvProperties.VIP_SATURN_NOHUPOUT_SIZE_LIMIT_IN_BYTES) {
                    LogUtils.info(log, TruncateLogRunnable.class.getCanonicalName(), "truncate {} as size over {} bytes", SystemEnvProperties.VIP_SATURN_LOG_OUTFILE, SystemEnvProperties.VIP_SATURN_NOHUPOUT_SIZE_LIMIT_IN_BYTES);
                    fc.truncate(TRUNCATE_SIZE);
                }
            } catch (FileNotFoundException e) {
                LogUtils.debug(log, TruncateLogRunnable.class.getCanonicalName(), "saturn-nohup.out is not found:", SystemEnvProperties.VIP_SATURN_LOG_OUTFILE, e);
            } catch (Exception e) {
                LogUtils.debug(log, TruncateLogRunnable.class.getCanonicalName(), "exception throws during handle saturn-nohup.out", e);
            }
        }
    }
}


{
        LogUtils.info(log, LogEvents.ExecutorEvent.SHUTDOWN, "shutdown PeriodicTruncateNohupOutService");
        if (truncateLogService != null) {
            truncateLogService.shutdownNow();
        }
    }

package com.vip.saturn.job.executor;

import com.vip.saturn.job.threads.SaturnThreadFactory;
import com.vip.saturn.job.utils.LogEvents;
import com.vip.saturn.job.utils.LogUtils;
import com.vip.saturn.job.utils.SystemEnvProperties;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Regular truncate saturn-nohup.out while the size is over specified limit.
 */
public class PeriodicTruncateNohupOutService {

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

    private static final long TRUNCATE_SIZE = 0;

    private ScheduledExecutorService truncateLogService;

    public PeriodicTruncateNohupOutService(String executorName) {
        truncateLogService = Executors.newScheduledThreadPool(1, new SaturnThreadFactory(executorName + "-truncate-nohup-out-thread", false));
    }

    public void start() {
        LogUtils.info(log, LogEvents.ExecutorEvent.INIT, "start PeriodicTruncateNohupOutService");
        if (StringUtils.isBlank(SystemEnvProperties.VIP_SATURN_LOG_OUTFILE)) {
            LogUtils.warn(log, LogEvents.ExecutorEvent.INIT, "File path of saturn-nohup.out is not set, please check.");
            return;
        }
        truncateLogService.scheduleAtFixedRate(new TruncateLogRunnable(), new Random().nextInt(10), SystemEnvProperties.VIP_SATURN_CHECK_NOHUPOUT_SIZE_INTERVAL_IN_SEC, TimeUnit.SECONDS);
    }

    public void shutdown()

    private class TruncateLogRunnable implements Runnable {

        @Override
        public void run() {
            try (RandomAccessFile file = new RandomAccessFile(SystemEnvProperties.VIP_SATURN_LOG_OUTFILE, "rw");
                FileChannel fc = file.getChannel()) {
                if (fc.size() > SystemEnvProperties.VIP_SATURN_NOHUPOUT_SIZE_LIMIT_IN_BYTES) {
                    LogUtils.info(log, TruncateLogRunnable.class.getCanonicalName(), "truncate {} as size over {} bytes", SystemEnvProperties.VIP_SATURN_LOG_OUTFILE, SystemEnvProperties.VIP_SATURN_NOHUPOUT_SIZE_LIMIT_IN_BYTES);
                    fc.truncate(TRUNCATE_SIZE);
                }
            } catch (FileNotFoundException e) {
                LogUtils.debug(log, TruncateLogRunnable.class.getCanonicalName(), "saturn-nohup.out is not found:", SystemEnvProperties.VIP_SATURN_LOG_OUTFILE, e);
            } catch (Exception e) {
                LogUtils.debug(log, TruncateLogRunnable.class.getCanonicalName(), "exception throws during handle saturn-nohup.out", e);
            }
        }
    }
}


package com.vip.saturn.job.executor;

import com.vip.saturn.job.threads.SaturnThreadFactory;
import com.vip.saturn.job.utils.LogEvents;
import com.vip.saturn.job.utils.LogUtils;
import com.vip.saturn.job.utils.SystemEnvProperties;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Regular truncate saturn-nohup.out while the size is over specified limit.
 */
public class PeriodicTruncateNohupOutService {

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

    private static final long TRUNCATE_SIZE = 0;

    private ScheduledExecutorService truncateLogService;

    public PeriodicTruncateNohupOutService(String executorName) {
        truncateLogService = Executors.newScheduledThreadPool(1, new SaturnThreadFactory(executorName + "-truncate-nohup-out-thread", false));
    }

    public void start() {
        LogUtils.info(log, LogEvents.ExecutorEvent.INIT, "start PeriodicTruncateNohupOutService");
        if (StringUtils.isBlank(SystemEnvProperties.VIP_SATURN_LOG_OUTFILE)) {
            LogUtils.warn(log, LogEvents.ExecutorEvent.INIT, "File path of saturn-nohup.out is not set, please check.");
            return;
        }
        truncateLogService.scheduleAtFixedRate(new TruncateLogRunnable(), new Random().nextInt(10), SystemEnvProperties

    }

    public void shutdown() {
        LogUtils.info(log, LogEvents.ExecutorEvent.SHUTDOWN, "shutdown PeriodicTruncateNohupOutService");
        if (truncateLogService != null) {
            truncateLogService.shutdownNow();
        }
    }

    private class TruncateLogRunnable implements Runnable {

        @Override
        public void run() {
            try (RandomAccessFile file = new RandomAccessFile(SystemEnvProperties.VIP_SATURN_LOG_OUTFILE, "rw");
                FileChannel fc = file.getChannel()) {
                if (fc.size() > SystemEnvProperties.VIP_SATURN_NOHUPOUT_SIZE_LIMIT_IN_BYTES) {
                    LogUtils.info(log, TruncateLogRunnable.class.getCanonicalName(), "truncate {} as size over {} bytes", SystemEnvProperties.VIP_SATURN_LOG_OUTFILE, SystemEnvProperties.VIP_SATURN_NOHUPOUT_SIZE_LIMIT_IN_BYTES);
                    fc.truncate(TRUNCATE_SIZE);
                }
            } catch (FileNotFoundException e) {
                LogUtils.debug(log, TruncateLogRunnable.class.getCanonicalName(), "saturn-nohup.out is not found:", SystemEnvProperties.VIP_SATURN_LOG_OUTFILE, e);
            } catch (Exception e) {
                LogUtils.debug(log, TruncateLogRunnable.class.getCanonicalName(), "exception throws during handle saturn-nohup.out", e);
            }
        }
    }
}


@Override
    public Installer findInstaller(String enterpriseId, String teamId, String userId)

package com.slack.api.bolt.service.builtin;

import com.slack.api.bolt.AppConfig;
import com.slack.api.bolt.model.Bot;
import com.slack.api.bolt.model.Installer;
import com.slack.api.bolt.model.builtin.DefaultBot;
import com.slack.api.bolt.model.builtin.DefaultInstaller;
import com.slack.api.bolt.service.InstallationService;
import com.slack.api.bolt.util.JsonOps;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import static java.util.stream.Collectors.joining;

@Slf4j
public class FileInstallationService implements InstallationService {

    public static final String DEFAULT_ROOT_DIR = System.getProperty("user.home") + File.separator + ".slack-app";

    private final AppConfig config;

    private final String rootDir;

    private boolean historicalDataEnabled;

    public FileInstallationService(AppConfig config) {
        this(config, DEFAULT_ROOT_DIR);
    }

    public FileInstallationService(AppConfig config, String rootDir) {
        this.config = config;
        this.rootDir = rootDir;
    }

    @Override
    public boolean isHistoricalDataEnabled() {
        return historicalDataEnabled;
    }

    @Override
    public void setHistoricalDataEnabled(boolean isHistoricalDataEnabled) {
        this.historicalDataEnabled = isHistoricalDataEnabled;
    }

    @Override
    public void saveInstallerAndBot(Installer installer) throws Exception {
        save(getInstallerPath(installer), installer.getInstalledAt(), JsonOps.toJsonString(installer));
        save(getBotPath(installer.getEnterpriseId(), installer.getTeamId()), installer.getInstalledAt(), JsonOps.toJsonString(installer.toBot()));
    }

    @Override
    public void saveBot(Bot bot) throws Exception {
        save(getBotPath(bot.getEnterpriseId(), bot.getTeamId()), bot.getInstalledAt(), JsonOps.toJsonString(bot));
    }

    @Override
    public void deleteBot(Bot bot) throws Exception {
        Files.deleteIfExists(Paths.get(getBotPath(bot.getEnterpriseId(), bot.getTeamId())));
    }

    @Override
    public void deleteInstaller(Installer installer) throws Exception {
        Files.deleteIfExists(Paths.get(getInstallerPath(installer)));
    }

    @Override
    public Bot findBot(String enterpriseId, String teamId) {
        try {
            String json = null;
            if (enterpriseId != null) {
                // try finding org-level bot token first
                try {
                    json = loadFileContent(getBotPath(enterpriseId, null));
                } catch (IOException e) {
                }
                if (json != null) {
                    return JsonOps.fromJson(json, DefaultBot.class);
                }
                // not found - going to find workspace level installation
            }
            try {
                json = loadFileContent(getBotPath(enterpriseId, teamId));
            } catch (IOException e) {
            }
            if (json == null && enterpriseId != null) {
                json = loadFileContent(getBotPath(null, teamId));
                if (json != null) {
                    Bot bot = JsonOps.fromJson(json, DefaultBot.class);
                    bot.setEnterpriseId(enterpriseId);
                    save(getBotPath(enterpriseId, teamId), bot.getInstalledAt(), JsonOps.toJsonString(bot));
                    return bot;
                }
            }
            if (json != null) {
                return JsonOps.fromJson(json, DefaultBot.class);
            } else {
                return null;
            }
        } catch (IOException e) {
            log.warn("Failed to load a bot user (enterprise_id: {}, team_id: {})", enterpriseId, teamId);
            return null;
        }
    }

{
        try {
            String json = null;
            if (enterpriseId != null) {
                // try finding org-level user token first
                try {
                    json = loadFileContent(getInstallerPath(enterpriseId, null, userId));
                } catch (IOException e) {
                }
                if (json != null) {
                    return JsonOps.fromJson(json, DefaultInstaller.class);
                }
                // not found - going to find workspace level installation
            }
            try {
                json = loadFileContent(getInstallerPath(enterpriseId, teamId, userId));
            } catch (IOException e) {
            }
            if (json == null && enterpriseId != null) {
                json = loadFileContent(getInstallerPath(null, teamId, userId));
                if (json != null) {
                    Installer i = JsonOps.fromJson(json, DefaultInstaller.class);
                    i.setEnterpriseId(enterpriseId);
                    save(getInstallerPath(enterpriseId, teamId, userId), i.getInstalledAt(), JsonOps.toJsonString(i));
                    return i;
                }
            }
            if (json != null) {
                return JsonOps.fromJson(json, DefaultInstaller.class);
            } else {
                return null;
            }
        } catch (IOException e) {
            log.warn("Failed to load an installer user (enterprise_id: {}, team_id: {})", enterpriseId, teamId);
            return null;
        }
    }

    @Override
    public void deleteAll(String enterpriseId, String teamId) {
        String keyPrefix = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none");
        deleteAllFilesMatchingPrefix(keyPrefix, getBaseDir() + File.separator + "installer");
        deleteAllFilesMatchingPrefix(keyPrefix, getBaseDir() + File.separator + "bot");
    }

    private static void deleteAllFilesMatchingPrefix(String keyPrefix, String dir) {
        try {
            Files.walk(Paths.get(dir)).filter(Files::isRegularFile).forEach(path -> {
                if (path.getFileName().startsWith(keyPrefix)) {
                    try {
                        Files.delete(path);
                    } catch (IOException e) {
                        log.error("Failed to delete a file: {}", path.toString(), e);
                    }
                }
            });
        } catch (IOException e) {
            log.error("Failed to scan files under installer directory: {}", dir);
        }
    }

    private String getInstallerPath(Installer i) throws IOException {
        return getInstallerPath(i.getEnterpriseId(), i.getTeamId(), i.getInstallerUserId());
    }

    private String getInstallerPath(String enterpriseId, String teamId, String userId) throws IOException {
        String dir = getBaseDir() + File.separator + "installer";
        Path dirPath = Paths.get(dir);
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
        }
        String key = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none") + "-" + userId;
        if (isHistoricalDataEnabled()) {
            key = key + "-latest";
        }
        return dir + File.separator + key;
    }

    private String getBotPath(String enterpriseId, String teamId) throws IOException {
        String dir = getBaseDir() + File.separator + "bot";
        Path dirPath = Paths.get(dir);
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
        }
        String key = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none");
        if (isHistoricalDataEnabled()) {
            key = key + "-latest";
        }
        return dir + File.separator + key;
    }

    private String getBaseDir() {
        return rootDir + File.separator + config.getClientId() + File.separator + "installation";
    }

    private void save(String path, Long installedAt, String json) throws IOException {
        // latest
        Files.write(Paths.get(path), json.getBytes());
        if (isHistoricalDataEnabled()) {
            // the historical data
            Files.write(Paths.get(path.replaceFirst("-latest$", "-" + installedAt)), json.getBytes());
        }
    }

    private String loadFileContent(String filepath) throws IOException {
        String content = Files.readAllLines(Paths.get(filepath)).stream().collect(joining());
        if (content == null || content.trim().isEmpty() || content.trim().equals("null")) {
            return null;
        }
        return content;
    }
}


{
        save(getInstallerPath(installer), installer.getInstalledAt(), JsonOps.toJsonString(installer));
        save(getBotPath(installer.getEnterpriseId(), installer.getTeamId()), installer.getInstalledAt(), JsonOps.toJsonString(installer.toBot()));
    }

package com.slack.api.bolt.service.builtin;

import com.slack.api.bolt.AppConfig;
import com.slack.api.bolt.model.Bot;
import com.slack.api.bolt.model.Installer;
import com.slack.api.bolt.model.builtin.DefaultBot;
import com.slack.api.bolt.model.builtin.DefaultInstaller;
import com.slack.api.bolt.service.InstallationService;
import com.slack.api.bolt.util.JsonOps;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import static java.util.stream.Collectors.joining;

@Slf4j
public class FileInstallationService implements InstallationService {

    public static final String DEFAULT_ROOT_DIR = System.getProperty("user.home") + File.separator + ".slack-app";

    private final AppConfig config;

    private final String rootDir;

    private boolean historicalDataEnabled;

    public FileInstallationService(AppConfig config) {
        this(config, DEFAULT_ROOT_DIR);
    }

    public FileInstallationService(AppConfig config, String rootDir) {
        this.config = config;
        this.rootDir = rootDir;
    }

    @Override
    public boolean isHistoricalDataEnabled() {
        return historicalDataEnabled;
    }

    @Override
    public void setHistoricalDataEnabled(boolean isHistoricalDataEnabled) {
        this.historicalDataEnabled = isHistoricalDataEnabled;
    }

    @Override
    public void saveInstallerAndBot(Installer installer) throws Exception

    @Override
    public void saveBot(Bot bot) throws Exception {
        save(getBotPath(bot.getEnterpriseId(), bot.getTeamId()), bot.getInstalledAt(), JsonOps.toJsonString(bot));
    }

    @Override
    public void deleteBot(Bot bot) throws Exception {
        Files.deleteIfExists(Paths.get(getBotPath(bot.getEnterpriseId(), bot.getTeamId())));
    }

    @Override
    public void deleteInstaller(Installer installer) throws Exception {
        Files.deleteIfExists(Paths.get(getInstallerPath(installer)));
    }

    @Override
    public Bot findBot(String enterpriseId, String teamId) {
        try {
            String json = null;
            if (enterpriseId != null) {
                // try finding org-level bot token first
                try {
                    json = loadFileContent(getBotPath(enterpriseId, null));
                } catch (IOException e) {
                }
                if (json != null) {
                    return JsonOps.fromJson(json, DefaultBot.class);
                }
                // not found - going to find workspace level installation
            }
            try {
                json = loadFileContent(getBotPath(enterpriseId, teamId));
            } catch (IOException e) {
            }
            if (json == null && enterpriseId != null) {
                json = loadFileContent(getBotPath(null, teamId));
                if (json != null) {
                    Bot bot = JsonOps.fromJson(json, DefaultBot.class);
                    bot.setEnterpriseId(enterpriseId);
                    save(getBotPath(enterpriseId, teamId), bot.getInstalledAt(), JsonOps.toJsonString(bot));
                    return bot;
                }
            }
            if (json != null) {
                return JsonOps.fromJson(json, DefaultBot.class);
            } else {
                return null;
            }
        } catch (IOException e) {
            log.warn("Failed to load a bot user (enterprise_id: {}, team_id: {})", enterpriseId, teamId);
            return null;
        }
    }

    @Override
    public Installer findInstaller(String enterpriseId, String teamId, String userId) {
        try {
            String json = null;
            if (enterpriseId != null) {
                // try finding org-level user token first
                try {
                    json = loadFileContent(getInstallerPath(enterpriseId, null, userId));
                } catch (IOException e) {
                }
                if (json != null) {
                    return JsonOps.fromJson(json, DefaultInstaller.class);
                }
                // not found - going to find workspace level installation
            }
            try {
                json = loadFileContent(getInstallerPath(enterpriseId, teamId, userId));
            } catch (IOException e) {
            }
            if (json == null && enterpriseId != null) {
                json = loadFileContent(getInstallerPath(null, teamId, userId));
                if (json != null) {
                    Installer i = JsonOps.fromJson(json, DefaultInstaller.class);
                    i.setEnterpriseId(enterpriseId);
                    save(getInstallerPath(enterpriseId, teamId, userId), i.getInstalledAt(), JsonOps.toJsonString(i));
                    return i;
                }
            }
            if (json != null) {
                return JsonOps.fromJson(json, DefaultInstaller.class);
            } else {
                return null;
            }
        } catch (IOException e) {
            log.warn("Failed to load an installer user (enterprise_id: {}, team_id: {})", enterpriseId, teamId);
            return null;
        }
    }

    @Override
    public void deleteAll(String enterpriseId, String teamId) {
        String keyPrefix = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none");
        deleteAllFilesMatchingPrefix(keyPrefix, getBaseDir() + File.separator + "installer");
        deleteAllFilesMatchingPrefix(keyPrefix, getBaseDir() + File.separator + "bot");
    }

    private static void deleteAllFilesMatchingPrefix(String keyPrefix, String dir) {
        try {
            Files.walk(Paths.get(dir)).filter(Files::isRegularFile).forEach(path -> {
                if (path.getFileName().startsWith(keyPrefix)) {
                    try {
                        Files.delete(path);
                    } catch (IOException e) {
                        log.error("Failed to delete a file: {}", path.toString(), e);
                    }
                }
            });
        } catch (IOException e) {
            log.error("Failed to scan files under installer directory: {}", dir);
        }
    }

    private String getInstallerPath(Installer i) throws IOException {
        return getInstallerPath(i.getEnterpriseId(), i.getTeamId(), i.getInstallerUserId());
    }

    private String getInstallerPath(String enterpriseId, String teamId, String userId) throws IOException {
        String dir = getBaseDir() + File.separator + "installer";
        Path dirPath = Paths.get(dir);
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
        }
        String key = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none") + "-" + userId;
        if (isHistoricalDataEnabled()) {
            key = key + "-latest";
        }
        return dir + File.separator + key;
    }

    private String getBotPath(String enterpriseId, String teamId) throws IOException {
        String dir = getBaseDir() + File.separator + "bot";
        Path dirPath = Paths.get(dir);
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
        }
        String key = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none");
        if (isHistoricalDataEnabled()) {
            key = key + "-latest";
        }
        return dir + File.separator + key;
    }

    private String getBaseDir() {
        return rootDir + File.separator + config.getClientId() + File.separator + "installation";
    }

    private void save(String path, Long installedAt, String json) throws IOException {
        // latest
        Files.write(Paths.get(path), json.getBytes());
        if (isHistoricalDataEnabled()) {
            // the historical data
            Files.write(Paths.get(path.replaceFirst("-latest$", "-" + installedAt)), json.getBytes());
        }
    }

    private String loadFileContent(String filepath) throws IOException {
        String content = Files.readAllLines(Paths.get(filepath)).stream().collect(joining());
        if (content == null || content.trim().isEmpty() || content.trim().equals("null")) {
            return null;
        }
        return content;
    }
}


package com.slack.api.bolt.service.builtin;

import com.slack.api.bolt.AppConfig;
import com.slack.api.bolt.model.Bot;
import com.slack.api.bolt.model.Installer;
import com.slack.api.bolt.model.builtin.DefaultBot;
import com.slack.api.bolt.model.builtin.DefaultInstaller;
import com.slack.api.bolt.service.InstallationService;
import com.slack.api.bolt.util.JsonOps;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import static java.util.stream.Collectors.joining;

@Slf4j
public class FileInstallationService implements InstallationService {

    public static final String DEFAULT_ROOT_DIR = System.getProperty("user.home") + File.separator + ".slack-app";

    private final AppConfig config;

    private final String rootDir;

    private boolean historicalDataEnabled;

    public FileInstallationService(AppConfig config) {
        this(config, DEFAULT_ROOT_DIR);
    }

    public FileInstallationService(AppConfig config, String rootDir) {
        this.config = config;
        this.rootDir = rootDir;
    }

    @Override
    public boolean isHistoricalDataEnabled() {
        return historicalDataEnabled;
    }

    @Override
    public void setHistoricalDataEnabled(boolean isHistoricalDataEnabled) {
        this.historicalDataEnabled = isHistoricalDataEnabled;
    }

    @Override
    public void saveInstallerAndBot(Installer installer) throws Exception {
        save(getInstallerPath(installer), installer.getInstalledAt(), JsonOps.toJsonString(installer));
        save(getBotPath(installer.getEnterpriseId(), installer.getTeamId()), installer.getInstalledAt(), JsonOps.toJsonString(installer.toBot()));
    }

    @Override
    public void saveBot(Bot bot) throws Exception {
        save(getBotPath(bot.getEnterpriseId(), bot.getTeamId()), bot.getInstalledAt(), JsonOps.toJsonString(bot));
    }

    @Override
    public void deleteBot(Bot bot) throws Exception {
        Files.deleteIfExists(Paths.get(getBotPath(bot.getEnterpriseId(), bot.getTeamId())));
    }

    @Override
    public void deleteInstaller(Installer installer) throws Exception {
        Files.deleteIfExists(Paths.

    }

    @Override
    public Bot findBot(String enterpriseId, String teamId) {
        try {
            String json = null;
            if (enterpriseId != null) {
                // try finding org-level bot token first
                try {
                    json = loadFileContent(getBotPath(enterpriseId, null));
                } catch (IOException e) {
                }
                if (json != null) {
                    return JsonOps.fromJson(json, DefaultBot.class);
                }
                // not found - going to find workspace level installation
            }
            try {
                json = loadFileContent(getBotPath(enterpriseId, teamId));
            } catch (IOException e) {
            }
            if (json == null && enterpriseId != null) {
                json = loadFileContent(getBotPath(null, teamId));
                if (json != null) {
                    Bot bot = JsonOps.fromJson(json, DefaultBot.class);
                    bot.setEnterpriseId(enterpriseId);
                    save(getBotPath(enterpriseId, teamId), bot.getInstalledAt(), JsonOps.toJsonString(bot));
                    return bot;
                }
            }
            if (json != null) {
                return JsonOps.fromJson(json, DefaultBot.class);
            } else {
                return null;
            }
        } catch (IOException e) {
            log.warn("Failed to load a bot user (enterprise_id: {}, team_id: {})", enterpriseId, teamId);
            return null;
        }
    }

    @Override
    public Installer findInstaller(String enterpriseId, String teamId, String userId) {
        try {
            String json = null;
            if (enterpriseId != null) {
                // try finding org-level user token first
                try {
                    json = loadFileContent(getInstallerPath(enterpriseId, null, userId));
                } catch (IOException e) {
                }
                if (json != null) {
                    return JsonOps.fromJson(json, DefaultInstaller.class);
                }
                // not found - going to find workspace level installation
            }
            try {
                json = loadFileContent(getInstallerPath(enterpriseId, teamId, userId));
            } catch (IOException e) {
            }
            if (json == null && enterpriseId != null) {
                json = loadFileContent(getInstallerPath(null, teamId, userId));
                if (json != null) {
                    Installer i = JsonOps.fromJson(json, DefaultInstaller.class);
                    i.setEnterpriseId(enterpriseId);
                    save(getInstallerPath(enterpriseId, teamId, userId), i.getInstalledAt(), JsonOps.toJsonString(i));
                    return i;
                }
            }
            if (json != null) {
                return JsonOps.fromJson(json, DefaultInstaller.class);
            } else {
                return null;
            }
        } catch (IOException e) {
            log.warn("Failed to load an installer user (enterprise_id: {}, team_id: {})", enterpriseId, teamId);
            return null;
        }
    }

    @Override
    public void deleteAll(String enterpriseId, String teamId) {
        String keyPrefix = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none");
        deleteAllFilesMatchingPrefix(keyPrefix, getBaseDir() + File.separator + "installer");
        deleteAllFilesMatchingPrefix(keyPrefix, getBaseDir() + File.separator + "bot");
    }

    private static void deleteAllFilesMatchingPrefix(String keyPrefix, String dir) {
        try {
            Files.walk(Paths.get(dir)).filter(Files::isRegularFile).forEach(path -> {
                if (path.getFileName().startsWith(keyPrefix)) {
                    try {
                        Files.delete(path);
                    } catch (IOException e) {
                        log.error("Failed to delete a file: {}", path.toString(), e);
                    }
                }
            });
        } catch (IOException e) {
            log.error("Failed to scan files under installer directory: {}", dir);
        }
    }

    private String getInstallerPath(Installer i) throws IOException {
        return getInstallerPath(i.getEnterpriseId(), i.getTeamId(), i.getInstallerUserId());
    }

    private String getInstallerPath(String enterpriseId, String teamId, String userId) throws IOException {
        String dir = getBaseDir() + File.separator + "installer";
        Path dirPath = Paths.get(dir);
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
        }
        String key = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none") + "-" + userId;
        if (isHistoricalDataEnabled()) {
            key = key + "-latest";
        }
        return dir + File.separator + key;
    }

    private String getBotPath(String enterpriseId, String teamId) throws IOException {
        String dir = getBaseDir() + File.separator + "bot";
        Path dirPath = Paths.get(dir);
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
        }
        String key = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none");
        if (isHistoricalDataEnabled()) {
            key = key + "-latest";
        }
        return dir + File.separator + key;
    }

    private String getBaseDir() {
        return rootDir + File.separator + config.getClientId() + File.separator + "installation";
    }

    private void save(String path, Long installedAt, String json) throws IOException {
        // latest
        Files.write(Paths.get(path), json.getBytes());
        if (isHistoricalDataEnabled()) {
            // the historical data
            Files.write(Paths.get(path.replaceFirst("-latest$", "-" + installedAt)), json.getBytes());
        }
    }

    private String loadFileContent(String filepath) throws IOException {
        String content = Files.readAllLines(Paths.get(filepath)).stream().collect(joining());
        if (content == null || content.trim().isEmpty() || content.trim().equals("null")) {
            return null;
        }
        return content;
    }
}


try {
                json = loadFileContent(getBotPath(enterpriseId, teamId));
            } catch (IOException e) {
            }

package com.slack.api.bolt.service.builtin;

import com.slack.api.bolt.AppConfig;
import com.slack.api.bolt.model.Bot;
import com.slack.api.bolt.model.Installer;
import com.slack.api.bolt.model.builtin.DefaultBot;
import com.slack.api.bolt.model.builtin.DefaultInstaller;
import com.slack.api.bolt.service.InstallationService;
import com.slack.api.bolt.util.JsonOps;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import static java.util.stream.Collectors.joining;

@Slf4j
public class FileInstallationService implements InstallationService {

    public static final String DEFAULT_ROOT_DIR = System.getProperty("user.home") + File.separator + ".slack-app";

    private final AppConfig config;

    private final String rootDir;

    private boolean historicalDataEnabled;

    public FileInstallationService(AppConfig config) {
        this(config, DEFAULT_ROOT_DIR);
    }

    public FileInstallationService(AppConfig config, String rootDir) {
        this.config = config;
        this.rootDir = rootDir;
    }

    @Override
    public boolean isHistoricalDataEnabled() {
        return historicalDataEnabled;
    }

    @Override
    public void setHistoricalDataEnabled(boolean isHistoricalDataEnabled) {
        this.historicalDataEnabled = isHistoricalDataEnabled;
    }

    @Override
    public void saveInstallerAndBot(Installer installer) throws Exception {
        save(getInstallerPath(installer), installer.getInstalledAt(), JsonOps.toJsonString(installer));
        save(getBotPath(installer.getEnterpriseId(), installer.getTeamId()), installer.getInstalledAt(), JsonOps.toJsonString(installer.toBot()));
    }

    @Override
    public void saveBot(Bot bot) throws Exception {
        save(getBotPath(bot.getEnterpriseId(), bot.getTeamId()), bot.getInstalledAt(), JsonOps.toJsonString(bot));
    }

    @Override
    public void deleteBot(Bot bot) throws Exception {
        Files.deleteIfExists(Paths.get(getBotPath(bot.getEnterpriseId(), bot.getTeamId())));
    }

    @Override
    public void deleteInstaller(Installer installer) throws Exception {
        Files.deleteIfExists(Paths.get(getInstallerPath(installer)));
    }

    @Override
    public Bot findBot(String enterpriseId, String teamId) {
        try {
            String json = null;
            if (enterpriseId != null) {
                // try finding org-level bot token first
                try {
                    json = loadFileContent(getBotPath(enterpriseId, null));
                } catch (IOException e) {
                }
                if (json != null) {
                    return JsonOps.fromJson(json, DefaultBot.class);
                }
                // not found - going to find workspace level installation
            }

            if (json == null && enterpriseId != null) {
                json = loadFileContent(getBotPath(null, teamId));
                if (json != null) {
                    Bot bot = JsonOps.fromJson(json, DefaultBot.class);
                    bot.setEnterpriseId(enterpriseId);
                    save(getBotPath(enterpriseId, teamId), bot.getInstalledAt(), JsonOps.toJsonString(bot));
                    return bot;
                }
            }
            if (json != null) {
                return JsonOps.fromJson(json, DefaultBot.class);
            } else {
                return null;
            }
        } catch (IOException e) {
            log.warn("Failed to load a bot user (enterprise_id: {}, team_id: {})", enterpriseId, teamId);
            return null;
        }
    }

    @Override
    public Installer findInstaller(String enterpriseId, String teamId, String userId) {
        try {
            String json = null;
            if (enterpriseId != null) {
                // try finding org-level user token first
                try {
                    json = loadFileContent(getInstallerPath(enterpriseId, null, userId));
                } catch (IOException e) {
                }
                if (json != null) {
                    return JsonOps.fromJson(json, DefaultInstaller.class);
                }
                // not found - going to find workspace level installation
            }
            try {
                json = loadFileContent(getInstallerPath(enterpriseId, teamId, userId));
            } catch (IOException e) {
            }
            if (json == null && enterpriseId != null) {
                json = loadFileContent(getInstallerPath(null, teamId, userId));
                if (json != null) {
                    Installer i = JsonOps.fromJson(json, DefaultInstaller.class);
                    i.setEnterpriseId(enterpriseId);
                    save(getInstallerPath(enterpriseId, teamId, userId), i.getInstalledAt(), JsonOps.toJsonString(i));
                    return i;
                }
            }
            if (json != null) {
                return JsonOps.fromJson(json, DefaultInstaller.class);
            } else {
                return null;
            }
        } catch (IOException e) {
            log.warn("Failed to load an installer user (enterprise_id: {}, team_id: {})", enterpriseId, teamId);
            return null;
        }
    }

    @Override
    public void deleteAll(String enterpriseId, String teamId) {
        String keyPrefix = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none");
        deleteAllFilesMatchingPrefix(keyPrefix, getBaseDir() + File.separator + "installer");
        deleteAllFilesMatchingPrefix(keyPrefix, getBaseDir() + File.separator + "bot");
    }

    private static void deleteAllFilesMatchingPrefix(String keyPrefix, String dir) {
        try {
            Files.walk(Paths.get(dir)).filter(Files::isRegularFile).forEach(path -> {
                if (path.getFileName().startsWith(keyPrefix)) {
                    try {
                        Files.delete(path);
                    } catch (IOException e) {
                        log.error("Failed to delete a file: {}", path.toString(), e);
                    }
                }
            });
        } catch (IOException e) {
            log.error("Failed to scan files under installer directory: {}", dir);
        }
    }

    private String getInstallerPath(Installer i) throws IOException {
        return getInstallerPath(i.getEnterpriseId(), i.getTeamId(), i.getInstallerUserId());
    }

    private String getInstallerPath(String enterpriseId, String teamId, String userId) throws IOException {
        String dir = getBaseDir() + File.separator + "installer";
        Path dirPath = Paths.get(dir);
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
        }
        String key = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none") + "-" + userId;
        if (isHistoricalDataEnabled()) {
            key = key + "-latest";
        }
        return dir + File.separator + key;
    }

    private String getBotPath(String enterpriseId, String teamId) throws IOException {
        String dir = getBaseDir() + File.separator + "bot";
        Path dirPath = Paths.get(dir);
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
        }
        String key = Optional.ofNullable(enterpriseId).orElse("none") + "-" + Optional.ofNullable(teamId).orElse("none");
        if (isHistoricalDataEnabled()) {
            key = key + "-latest";
        }
        return dir + File.separator + key;
    }

    private String getBaseDir() {
        return rootDir + File.separator + config.getClientId() + File.separator + "installation";
    }

    private void save(String path, Long installedAt, String json) throws IOException {
        // latest
        Files.write(Paths.get(path), json.getBytes());
        if (isHistoricalDataEnabled()) {
            // the historical data
            Files.write(Paths.get(path.replaceFirst("-latest$", "-" + installedAt)), json.getBytes());
        }
    }

    private String loadFileContent(String filepath) throws IOException {
        String content = Files.readAllLines(Paths.get(filepath)).stream().collect(joining());
        if (content == null || content.trim().isEmpty() || content.trim().equals("null")) {
            return null;
        }
        return content;
    }
}


package javax.net.ssl;

import java.net.IDN;
import java.nio.ByteBuffer;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharacterCodingException;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * Instances of this class represent a server name of type
 * {@link StandardConstants#SNI_HOST_NAME host_name} in a Server Name
 * Indication (SNI) extension.
 *

 * As described in section 3, "Server Name Indication", of
 * TLS Extensions (RFC 6066),
 * "HostName" contains the fully qualified DNS hostname of the server, as
 * understood by the client.  The encoded server name value of a hostname is
 * represented as a byte string using ASCII encoding without a trailing dot.
 * This allows the support of Internationalized Domain Names (IDN) through
 * the use of A-labels (the ASCII-Compatible Encoding (ACE) form of a valid
 * string of Internationalized Domain Names for Applications (IDNA)) defined
 * in RFC 5890.
 *

 * Note that {@code SNIHostName} objects are immutable.
 *
 * @see SNIServerName
 * @see StandardConstants#SNI_HOST_NAME
 *
 * @since 1.8
 */
public final class SNIHostName extends SNIServerName {

    // the decoded string value of the server name
    private final String hostname;

    /**
     * Creates an {@code SNIHostName} using the specified hostname.
     *

     * Note that per RFC 6066,
     * the encoded server name value of a hostname is
     * {@link StandardCharsets#US_ASCII}-compliant.  In this method,
     * {@code hostname} can be a user-friendly Internationalized Domain Name
     * (IDN).  {@link IDN#toASCII(String, int)} is used to enforce the
     * restrictions on ASCII characters in hostnames (see
     * RFC 3490,
     * RFC 1122,
     * RFC 1123) and
     * translate the {@code hostname} into ASCII Compatible Encoding (ACE), as:
     *

     *     IDN.toASCII(hostname, IDN.USE_STD3_ASCII_RULES);
     *

     *

     * The {@code hostname} argument is illegal if it:
     *

     *
 {@code hostname} is empty,

     *
 {@code hostname} ends with a trailing dot,

     *
 {@code hostname} is not a valid Internationalized
     *      Domain Name (IDN) compliant with the RFC 3490 specification.

     *

     * @param  hostname
     *         the hostname of this server name
     *
     * @throws NullPointerException if {@code hostname} is {@code null}
     * @throws IllegalArgumentException if {@code hostname} is illegal
     */
    public SNIHostName(String hostname) {
        // IllegalArgumentException will be thrown if {@code hostname} is
        // not a valid IDN.
        super(StandardConstants.SNI_HOST_NAME, (hostname = IDN.toASCII(Objects.requireNonNull(hostname, "Server name value of host_name cannot be null"), IDN.USE_STD3_ASCII_RULES)).getBytes(StandardCharsets.US_ASCII));
        this.hostname = hostname;
        // check the validity of the string hostname
        checkHostName();
    }

    /**
     * Creates an {@code SNIHostName} using the specified encoded value.
     *

     * This method is normally used to parse the encoded name value in a
     * requested SNI extension.
     *

     * Per RFC 6066,
     * the encoded name value of a hostname is
     * {@link StandardCharsets#US_ASCII}-compliant.  However, in the previous
     * version of the SNI extension (
     * RFC 4366),
     * the encoded hostname is represented as a byte string using UTF-8
     * encoding.  For the purpose of version tolerance, this method allows
     * that the charset of {@code encoded} argument can be
     * {@link StandardCharsets#UTF_8}, as well as
     * {@link StandardCharsets#US_ASCII}.  {@link IDN#toASCII(String)} is used
     * to translate the {@code encoded} argument into ASCII Compatible
     * Encoding (ACE) hostname.
     *

     * It is strongly recommended that this constructor is only used to parse
     * the encoded name value in a requested SNI extension.  Otherwise, to
     * comply with RFC 6066,
     * please always use {@link StandardCharsets#US_ASCII}-compliant charset
     * and enforce the restrictions on ASCII characters in hostnames (see
     * RFC 3490,
     * RFC 1122,
     * RFC 1123)
     * for {@code encoded} argument, or use
     * {@link SNIHostName#SNIHostName(String)} instead.
     *

     * The {@code encoded} argument is illegal if it:
     *

     *
 {@code encoded} is empty,

     *
 {@code encoded} ends with a trailing dot,

     *
 {@code encoded} is not encoded in
     *      {@link StandardCharsets#US_ASCII} or
     *      {@link StandardCharsets#UTF_8}-compliant charset,

     *
 {@code encoded} is not a valid Internationalized
     *      Domain Name (IDN) compliant with the RFC 3490 specification.

     *

     *
     *

     * Note that the {@code encoded} byte array is cloned
     * to protect against subsequent modification.
     *
     * @param  encoded
     *         the encoded hostname of this server name
     *
     * @throws NullPointerException if {@code encoded} is {@code null}
     * @throws IllegalArgumentException if {@code encoded} is illegal
     */
    public SNIHostName(byte[] encoded) {
        // NullPointerException will be thrown if {@code encoded} is null
        super(StandardConstants.SNI_HOST_NAME, encoded);
        // Compliance: RFC 4366 requires that the hostname is represented
        // as a byte string using UTF_8 encoding [UTF8]
        try {
            // Please don't use {@link String} constructors because they
            // do not report coding errors.
            CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
            this.hostname = IDN.toASCII(decoder.decode(ByteBuffer.wrap(encoded)).toString(), IDN.USE_STD3_ASCII_RULES);
        } catch (RuntimeException | CharacterCodingException e) {
            throw new IllegalArgumentException("The encoded server name value is invalid", e);
        }
        // check the validity of the string hostname
        checkHostName();
    }

    /**
     * Returns the {@link StandardCharsets#US_ASCII}-compliant hostname of
     * this {@code SNIHostName} object.
     *

     * Note that, per
     * RFC 6066, the
     * returned hostname may be an internationalized domain name that
     * contains A-labels. See
     * RFC 5890
     * for more information about the detailed A-label specification.
     *
     * @return the {@link StandardCharsets#US_ASCII}-compliant hostname
     *         of this {@code SNIHostName} object
     */
    public String getAsciiName() {
        return hostname;
    }

    /**
     * Compares this server name to the specified object.
     *

     * Per RFC 6066, DNS
     * hostnames are case-insensitive.  Two server hostnames are equal if,
     * and only if, they have the same name type, and the hostnames are
     * equal in a case-independent comparison.
     *
     * @param  other
     *         the other server name object to compare with.
     * @return true if, and only if, the {@code other} is considered
     *         equal to this instance
     */
    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (other instanceof SNIHostName) {
            return hostname.equalsIgnoreCase(((SNIHostName) other).hostname);
        }
        return false;
    }

    /**
     * Returns a hash code value for this {@code SNIHostName}.
     *

     * The hash code value is generated using the case-insensitive hostname
     * of this {@code SNIHostName}.
     *
     * @return a hash code value for this {@code SNIHostName}.
     */

{
        // 17/31: prime number to decrease collisions
        int result = 17;
        result = 31 * result + hostname.toUpperCase(Locale.ENGLISH).hashCode();
        return result;
    }

    /**
     * Returns a string representation of the object, including the DNS
     * hostname in this {@code SNIHostName} object.
     *

     * The exact details of the representation are unspecified and subject
     * to change, but the following may be regarded as typical:
     *

     *     "type=host_name (0), value={@literal <hostname>}"
     *

     * The "{@literal <hostname>}" is an ASCII representation of the hostname,
     * which may contains A-labels.  For example, a returned value of an pseudo
     * hostname may look like:
     *

     *     "type=host_name (0), value=www.example.com"
     *

     * or
     *

     *     "type=host_name (0), value=xn--fsqu00a.xn--0zwm56d"
     *

     *

     * Please NOTE that the exact details of the representation are unspecified
     * and subject to change.
     *
     * @return a string representation of the object.
     */
    @Override
    public String toString() {
        return "type=host_name (0), value=" + hostname;
    }

    /**
     * Creates an {@link SNIMatcher} object for {@code SNIHostName}s.
     *

     * This method can be used by a server to verify the acceptable
     * {@code SNIHostName}s.  For example,
     *

     *     SNIMatcher matcher =
     *         SNIHostName.createSNIMatcher("www\\.example\\.com");
     *

     * will accept the hostname "www.example.com".
     *

     *     SNIMatcher matcher =
     *         SNIHostName.createSNIMatcher("www\\.example\\.(com|org)");
     *

     * will accept hostnames "www.example.com" and "www.example.org".
     *
     * @param  regex
     *         the
     *         regular expression pattern
     *         representing the hostname(s) to match
     * @return a {@code SNIMatcher} object for {@code SNIHostName}s
     * @throws NullPointerException if {@code regex} is
     *         {@code null}
     * @throws PatternSyntaxException if the regular expression's
     *         syntax is invalid
     */
    public static SNIMatcher createSNIMatcher(String regex) {
        if (regex == null) {
            throw new NullPointerException("The regular expression cannot be null");
        }
        return new SNIHostNameMatcher(regex);
    }

    // check the validity of the string hostname
    private void checkHostName() {
        if (hostname.isEmpty()) {
            throw new IllegalArgumentException("Server name value of host_name cannot be empty");
        }
        if (hostname.endsWith(".")) {
            throw new IllegalArgumentException("Server name value of host_name cannot have the trailing dot");
        }
    }

    private static final class SNIHostNameMatcher extends SNIMatcher {

        // the compiled representation of a regular expression.
        private final Pattern pattern;

        /**
         * Creates an SNIHostNameMatcher object.
         *
         * @param  regex
         *         the
         *         regular expression pattern
         *         representing the hostname(s) to match
         * @throws NullPointerException if {@code regex} is
         *         {@code null}
         * @throws PatternSyntaxException if the regular expression's syntax
         *         is invalid
         */
        SNIHostNameMatcher(String regex) {
            super(StandardConstants.SNI_HOST_NAME);
            pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        }

        /**
         * Attempts to match the given {@link SNIServerName}.
         *
         * @param  serverName
         *         the {@link SNIServerName} instance on which this matcher
         *         performs match operations
         *
         * @return {@code true} if, and only if, the matcher matches the
         *         given {@code serverName}
         *
         * @throws NullPointerException if {@code serverName} is {@code null}
         * @throws IllegalArgumentException if {@code serverName} is
         *         not of {@code StandardConstants#SNI_HOST_NAME} type
         *
         * @see SNIServerName
         */
        @Override
        public boolean matches(SNIServerName serverName) {
            if (serverName == null) {
                throw new NullPointerException("The SNIServerName argument cannot be null");
            }
            SNIHostName hostname;
            if (!(serverName instanceof SNIHostName)) {
                if (serverName.getType() != StandardConstants.SNI_HOST_NAME) {
                    throw new IllegalArgumentException("The server name type is not host_name");
                }
                try {
                    hostname = new SNIHostName(serverName.getEncoded());
                } catch (NullPointerException | IllegalArgumentException e) {
                    return false;
                }
            } else {
                hostname = (SNIHostName) serverName;
            }
            // Let's first try the ascii name matching
            String asciiName = hostname.getAsciiName();
            if (pattern.matcher(asciiName).matches()) {
                return true;
            }
            // May be an internationalized domain name, check the Unicode
            return pattern.matcher(IDN.toUnicode(asciiName)).matches();
        }
    }
}


{
        // 17/31: prime number to decrease collisions
        int result = 17;
        result = 31 * result + hostname.toUpperCase(Locale.ENGLISH).hashCode();
        return result;
    }

package javax.net.ssl;

import java.net.IDN;
import java.nio.ByteBuffer;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharacterCodingException;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * Instances of this class represent a server name of type
 * {@link StandardConstants#SNI_HOST_NAME host_name} in a Server Name
 * Indication (SNI) extension.
 *

 * As described in section 3, "Server Name Indication", of
 * TLS Extensions (RFC 6066),
 * "HostName" contains the fully qualified DNS hostname of the server, as
 * understood by the client.  The encoded server name value of a hostname is
 * represented as a byte string using ASCII encoding without a trailing dot.
 * This allows the support of Internationalized Domain Names (IDN) through
 * the use of A-labels (the ASCII-Compatible Encoding (ACE) form of a valid
 * string of Internationalized Domain Names for Applications (IDNA)) defined
 * in RFC 5890.
 *

 * Note that {@code SNIHostName} objects are immutable.
 *
 * @see SNIServerName
 * @see StandardConstants#SNI_HOST_NAME
 *
 * @since 1.8
 */
public final class SNIHostName extends SNIServerName {

    // the decoded string value of the server name
    private final String hostname;

    /**
     * Creates an {@code SNIHostName} using the specified hostname.
     *

     * Note that per RFC 6066,
     * the encoded server name value of a hostname is
     * {@link StandardCharsets#US_ASCII}-compliant.  In this method,
     * {@code hostname} can be a user-friendly Internationalized Domain Name
     * (IDN).  {@link IDN#toASCII(String, int)} is used to enforce the
     * restrictions on ASCII characters in hostnames (see
     * RFC 3490,
     * RFC 1122,
     * RFC 1123) and
     * translate the {@code hostname} into ASCII Compatible Encoding (ACE), as:
     *

     *     IDN.toASCII(hostname, IDN.USE_STD3_ASCII_RULES);
     *

     *

     * The {@code hostname} argument is illegal if it:
     *

     *
 {@code hostname} is empty,

     *
 {@code hostname} ends with a trailing dot,

     *
 {@code hostname} is not a valid Internationalized
     *      Domain Name (IDN) compliant with the RFC 3490 specification.

     *

     * @param  hostname
     *         the hostname of this server name
     *
     * @throws NullPointerException if {@code hostname} is {@code null}
     * @throws IllegalArgumentException if {@code hostname} is illegal
     */
    public SNIHostName(String hostname) {
        // IllegalArgumentException will be thrown if {@code hostname} is
        // not a valid IDN.
        super(StandardConstants.SNI_HOST_NAME, (hostname = IDN.toASCII(Objects.requireNonNull(hostname, "Server name value of host_name cannot be null"), IDN.USE_STD3_ASCII_RULES)).getBytes(StandardCharsets.US_ASCII));
        this.hostname = hostname;
        // check the validity of the string hostname
        checkHostName();
    }

    /**
     * Creates an {@code SNIHostName} using the specified encoded value.
     *

     * This method is normally used to parse the encoded name value in a
     * requested SNI extension.
     *

     * Per RFC 6066,
     * the encoded name value of a hostname is
     * {@link StandardCharsets#US_ASCII}-compliant.  However, in the previous
     * version of the SNI extension (
     * RFC 4366),
     * the encoded hostname is represented as a byte string using UTF-8
     * encoding.  For the purpose of version tolerance, this method allows
     * that the charset of {@code encoded} argument can be
     * {@link StandardCharsets#UTF_8}, as well as
     * {@link StandardCharsets#US_ASCII}.  {@link IDN#toASCII(String)} is used
     * to translate the {@code encoded} argument into ASCII Compatible
     * Encoding (ACE) hostname.
     *

     * It is strongly recommended that this constructor is only used to parse
     * the encoded name value in a requested SNI extension.  Otherwise, to
     * comply with RFC 6066,
     * please always use {@link StandardCharsets#US_ASCII}-compliant charset
     * and enforce the restrictions on ASCII characters in hostnames (see
     * RFC 3490,
     * RFC 1122,
     * RFC 1123)
     * for {@code encoded} argument, or use
     * {@link SNIHostName#SNIHostName(String)} instead.
     *

     * The {@code encoded} argument is illegal if it:
     *

     *
 {@code encoded} is empty,

     *
 {@code encoded} ends with a trailing dot,

     *
 {@code encoded} is not encoded in
     *      {@link StandardCharsets#US_ASCII} or
     *      {@link StandardCharsets#UTF_8}-compliant charset,

     *
 {@code encoded} is not a valid Internationalized
     *      Domain Name (IDN) compliant with the RFC 3490 specification.

     *

     *
     *

     * Note that the {@code encoded} byte array is cloned
     * to protect against subsequent modification.
     *
     * @param  encoded
     *         the encoded hostname of this server name
     *
     * @throws NullPointerException if {@code encoded} is {@code null}
     * @throws IllegalArgumentException if {@code encoded} is illegal
     */
    public SNIHostName(byte[] encoded) {
        // NullPointerException will be thrown if {@code encoded} is null
        super(StandardConstants.SNI_HOST_NAME, encoded);
        // Compliance: RFC 4366 requires that the hostname is represented
        // as a byte string using UTF_8 encoding [UTF8]
        try {
            // Please don't use {@link String} constructors because they
            // do not report coding errors.
            CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
            this.hostname = IDN.toASCII(decoder.decode(ByteBuffer.wrap(encoded)).toString(), IDN.USE_STD3_ASCII_RULES);
        } catch (RuntimeException | CharacterCodingException e) {
            throw new IllegalArgumentException("The encoded server name value is invalid", e);
        }
        // check the validity of the string hostname
        checkHostName();
    }

    /**
     * Returns the {@link StandardCharsets#US_ASCII}-compliant hostname of
     * this {@code SNIHostName} object.
     *

     * Note that, per
     * RFC 6066, the
     * returned hostname may be an internationalized domain name that
     * contains A-labels. See
     * RFC 5890
     * for more information about the detailed A-label specification.
     *
     * @return the {@link StandardCharsets#US_ASCII}-compliant hostname
     *         of this {@code SNIHostName} object
     */
    public String getAsciiName() {
        return hostname;
    }

    /**
     * Compares this server name to the specified object.
     *

     * Per RFC 6066, DNS
     * hostnames are case-insensitive.  Two server hostnames are equal if,
     * and only if, they have the same name type, and the hostnames are
     * equal in a case-independent comparison.
     *
     * @param  other
     *         the other server name object to compare with.
     * @return true if, and only if, the {@code other} is considered
     *         equal to this instance
     */
    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (other instanceof SNIHostName) {
            return hostname.equalsIgnoreCase(((SNIHostName) other).hostname);
        }
        return false;
    }

    /**
     * Returns a hash code value for this {@code SNIHostName}.
     *

     * The hash code value is generated using the case-insensitive hostname
     * of this {@code SNIHostName}.
     *
     * @return a hash code value for this {@code SNIHostName}.
     */
    @Override
    public int hashCode()

/**
     * Returns a string representation of the object, including the DNS
     * hostname in this {@code SNIHostName} object.
     *

     * The exact details of the representation are unspecified and subject
     * to change, but the following may be regarded as typical:
     *

     *     "type=host_name (0), value={@literal <hostname>}"
     *

     * The "{@literal <hostname>}" is an ASCII representation of the hostname,
     * which may contains A-labels.  For example, a returned value of an pseudo
     * hostname may look like:
     *

     *     "type=host_name (0), value=www.example.com"
     *

     * or
     *

     *     "type=host_name (0), value=xn--fsqu00a.xn--0zwm56d"
     *

     *

     * Please NOTE that the exact details of the representation are unspecified
     * and subject to change.
     *
     * @return a string representation of the object.
     */
    @Override
    public String toString() {
        return "type=host_name (0), value=" + hostname;
    }

    /**
     * Creates an {@link SNIMatcher} object for {@code SNIHostName}s.
     *

     * This method can be used by a server to verify the acceptable
     * {@code SNIHostName}s.  For example,
     *

     *     SNIMatcher matcher =
     *         SNIHostName.createSNIMatcher("www\\.example\\.com");
     *

     * will accept the hostname "www.example.com".
     *

     *     SNIMatcher matcher =
     *         SNIHostName.createSNIMatcher("www\\.example\\.(com|org)");
     *

     * will accept hostnames "www.example.com" and "www.example.org".
     *
     * @param  regex
     *         the
     *         regular expression pattern
     *         representing the hostname(s) to match
     * @return a {@code SNIMatcher} object for {@code SNIHostName}s
     * @throws NullPointerException if {@code regex} is
     *         {@code null}
     * @throws PatternSyntaxException if the regular expression's
     *         syntax is invalid
     */
    public static SNIMatcher createSNIMatcher(String regex) {
        if (regex == null) {
            throw new NullPointerException("The regular expression cannot be null");
        }
        return new SNIHostNameMatcher(regex);
    }

    // check the validity of the string hostname
    private void checkHostName() {
        if (hostname.isEmpty()) {
            throw new IllegalArgumentException("Server name value of host_name cannot be empty");
        }
        if (hostname.endsWith(".")) {
            throw new IllegalArgumentException("Server name value of host_name cannot have the trailing dot");
        }
    }

    private static final class SNIHostNameMatcher extends SNIMatcher {

        // the compiled representation of a regular expression.
        private final Pattern pattern;

        /**
         * Creates an SNIHostNameMatcher object.
         *
         * @param  regex
         *         the
         *         regular expression pattern
         *         representing the hostname(s) to match
         * @throws NullPointerException if {@code regex} is
         *         {@code null}
         * @throws PatternSyntaxException if the regular expression's syntax
         *         is invalid
         */
        SNIHostNameMatcher(String regex) {
            super(StandardConstants.SNI_HOST_NAME);
            pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        }

        /**
         * Attempts to match the given {@link SNIServerName}.
         *
         * @param  serverName
         *         the {@link SNIServerName} instance on which this matcher
         *         performs match operations
         *
         * @return {@code true} if, and only if, the matcher matches the
         *         given {@code serverName}
         *
         * @throws NullPointerException if {@code serverName} is {@code null}
         * @throws IllegalArgumentException if {@code serverName} is
         *         not of {@code StandardConstants#SNI_HOST_NAME} type
         *
         * @see SNIServerName
         */
        @Override
        public boolean matches(SNIServerName serverName) {
            if (serverName == null) {
                throw new NullPointerException("The SNIServerName argument cannot be null");
            }
            SNIHostName hostname;
            if (!(serverName instanceof SNIHostName)) {
                if (serverName.getType() != StandardConstants.SNI_HOST_NAME) {
                    throw new IllegalArgumentException("The server name type is not host_name");
                }
                try {
                    hostname = new SNIHostName(serverName.getEncoded());
                } catch (NullPointerException | IllegalArgumentException e) {
                    return false;
                }
            } else {
                hostname = (SNIHostName) serverName;
            }
            // Let's first try the ascii name matching
            String asciiName = hostname.getAsciiName();
            if (pattern.matcher(asciiName).matches()) {
                return true;
            }
            // May be an internationalized domain name, check the Unicode
            return pattern.matcher(IDN.toUnicode(asciiName)).matches();
        }
    }
}