@VisibleForTesting
public class ObjectMappers
{
    private ObjectMappers()
    {
    }

    /**
     * Lazy instantiation wrapper.
     */
    private static class Lazy
    {
        private static final ObjectMapper COMPACT_MAPPER = createCompactMapper();
    }

    /**
     * Get an {@link ObjectMapper} that omits null and absent fields.
     */
    @VisibleForTesting
    public static ObjectMapper compactMapper()
    {
        return Lazy.COMPACT_MAPPER;
    }

    private static ObjectMapper createCompactMapper()
    {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JsonOrgModule());
        mapper.registerModule(new GuavaModule().configureAbsentsAsNulls(false));
        mapper.setSerializationInclusion(JsonInclude.Include.NON_ABSENT);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return mapper;
    }
} @Value.Immutable
@Value.Style(visibility = Value.Style.ImplementationVisibility.PACKAGE)
@JsonSerialize(as = ImmutableTDApiKey.class)
@JsonDeserialize(as = ImmutableTDApiKey.class)
public abstract class TDApiKey
{
    @JsonProperty("key_type") public abstract String getKeyType();
    @JsonProperty("account_id") public abstract Integer getAccountId();
    @JsonProperty("user_id") public abstract Integer getUserId();
    @JsonProperty("administrator") public abstract boolean isAdministrator();

    public interface Builder
    {
        Builder keyType(String keyType);
        Builder accountId(Integer accountId);
        Builder userId(Integer userId);
        Builder isAdministrator(boolean administrator);
        TDApiKey build();
    }

    public static Builder builder()
    {
        return ImmutableTDApiKey.builder();
    }
} public class TDAuthenticationResult
{
    private final String name;
    private final String apikey;

    @JsonCreator
    public TDAuthenticationResult(@JsonProperty("name") String name, @JsonProperty("apikey") String apikey)
    {
        this.name = name;
        this.apikey = apikey;
    }

    public String getName()
    {
        return name;
    }

    public String getApikey()
    {
        return apikey;
    }

    @Override
    public String toString()
    {
        return "TDAuthenticationResult{" +
                "name='" + name + '\'' +
                ", apikey='" + apikey + '\'' +
                '}';
    }
} public class TDBulkImportParts
{
    private final List<String> parts;

    @JsonCreator
    public TDBulkImportParts(@JsonProperty("parts") List<String> parts)
    {
        this.parts = parts;
    }

    public List<String> getParts()
    {
        return parts;
    }
} @JsonCollectionRootName(value = "bulk_imports")
public class TDBulkImportSession
{
    public static enum ImportStatus
    {
        UPLOADING("uploading"),
        PERFORMING("performing"),
        READY("ready"),
        COMMITTING("committing"),
        COMMITTED("committed"),
        UNKNOWN("unknown");
        private final String name;

        private ImportStatus(String name)
        {
            this.name = name;
        }

        @JsonCreator
        public static ImportStatus forName(String name)
        {
            return ImportStatus.valueOf(name.toUpperCase());
        }
    }

    private final String name;
    private final String databaseName;
    private final String tableName;
    private final ImportStatus status;
    private final boolean uploadFrozen;
    private final String jobId; // nullable
    private final long validRecords;
    private final long errorRecords;
    private final long validParts;
    private final long errorParts;

    @JsonCreator
    public TDBulkImportSession(
            @JsonProperty("name") String name,
            @JsonProperty("database") String databaseName,
            @JsonProperty("table") String tableName,
            @JsonProperty("status") ImportStatus status,
            @JsonProperty("upload_frozen") boolean uploadFrozen,
            @JsonProperty("job_id") String jobId,
            @JsonProperty("valid_records") long validRecords,
            @JsonProperty("error_records") long errorRecords,
            @JsonProperty("valid_parts") long validParts,
            @JsonProperty("error_parts") long errorParts)
    {
        this.name = name;
        this.databaseName = databaseName;
        this.tableName = tableName;
        this.status = status;
        this.uploadFrozen = uploadFrozen;
        this.jobId = jobId;
        this.validRecords = validRecords;
        this.errorRecords = errorRecords;
        this.validParts = validParts;
        this.errorParts = errorParts;
    }

    @JsonProperty
    public String getName()
    {
        return name;
    }

    @JsonProperty("database")
    public String getDatabaseName()
    {
        return databaseName;
    }

    @JsonProperty("table")
    public String getTableName()
    {
        return tableName;
    }

    @JsonProperty
    public ImportStatus getStatus()
    {
        return status;
    }

    @JsonProperty("upload_frozen")
    public boolean isUploadFrozen()
    {
        return uploadFrozen;
    }

    @JsonProperty("job_id")
    public String getJobId()
    {
        return jobId;
    }

    @JsonProperty("valid_records")
    public long getValidRecords()
    {
        return validRecords;
    }

    @JsonProperty("error_records")
    public long getErrorRecords()
    {
        return errorRecords;
    }

    @JsonProperty("valid_parts")
    public long getValidParts()
    {
        return validParts;
    }

    @JsonProperty("error_parts")
    public long getErrorParts()
    {
        return errorParts;
    }

    public boolean isUploading()
    {
        return status == ImportStatus.UPLOADING;
    }

    public boolean hasErrorOnPerform()
    {
        return validRecords == 0 || errorParts > 0 || errorRecords > 0;
    }

    public String getErrorMessage()
    {
        if (validRecords == 0) {
            return "No record processed";
        }
        if (errorParts > 0) {
            return String.format("%d invalid parts", errorParts);
        }
        if (errorRecords > 0) {
            return String.format("%d invalid records", errorRecords);
        }

        return null;
    }
} public class TDBulkLoadSessionStartRequest
{
    private final String scheduledTime;
    private final String domainKey;

    @JsonCreator
    TDBulkLoadSessionStartRequest(
            @JsonProperty("scheduled_time") String scheduledTime,
            @JsonProperty("domain_key") String domainKey
            )
    {
        this.scheduledTime = scheduledTime;
        this.domainKey = domainKey;
    }

    TDBulkLoadSessionStartRequest(TDBulkLoadSessionStartRequestBuilder builder)
    {
        this.scheduledTime = builder.getScheduledTime().orNull();
        this.domainKey = builder.getDomainKey().orNull();
    }

    @JsonProperty("scheduled_time")
    public Optional<String> getScheduledTime()
    {
        return Optional.fromNullable(scheduledTime);
    }

    @JsonProperty("domain_key")
    public Optional<String> getDomainKey()
    {
        return Optional.fromNullable(domainKey);
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        TDBulkLoadSessionStartRequest that = (TDBulkLoadSessionStartRequest) o;

        return scheduledTime != null ? scheduledTime.equals(that.scheduledTime) : that.scheduledTime == null;
    }

    @Override
    public int hashCode()
    {
        return scheduledTime != null ? scheduledTime.hashCode() : 0;
    }

    @Override
    public String toString()
    {
        return "TDBulkLoadSessionStartRequest{" +
                "scheduledTime='" + scheduledTime + '\'' +
                ", domainKey='" + domainKey + '\'' +
                '}';
    }

    public static TDBulkLoadSessionStartRequestBuilder builder()
    {
        return new TDBulkLoadSessionStartRequestBuilder();
    }

    public static TDBulkLoadSessionStartRequest of()
    {
        return builder().build();
    }
} public class TDBulkLoadSessionStartResult
{
    private final String jobId;

    @JsonCreator
    TDBulkLoadSessionStartResult(
            @JsonProperty("job_id") String jobId)
    {
        this.jobId = jobId;
    }

    @JsonProperty("job_id")
    public String getJobId()
    {
        return jobId;
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        TDBulkLoadSessionStartResult that = (TDBulkLoadSessionStartResult) o;

        return jobId != null ? jobId.equals(that.jobId) : that.jobId == null;
    }

    @Override
    public int hashCode()
    {
        return jobId != null ? jobId.hashCode() : 0;
    }

    @Override
    public String toString()
    {
        return "TDBulkLoadSessionStartResult{" +
                "jobId='" + jobId + '\'' +
                '}';
    }

    public static TDBulkLoadSessionStartResult of(String jobId)
    {
        return new TDBulkLoadSessionStartResult(jobId);
    }
} public class TDColumn implements Serializable
{
    static final byte[] LOG_TABLE_PUSHDOWN_KEY = "time".getBytes(UTF_8);

    // The column SQL alias.
    private final String name;
    private final TDColumnType type;
    // The physical column name in partition files
    private final byte[] key;

    public TDColumn(String name, TDColumnType type)
    {
        this(name, type, name.getBytes(StandardCharsets.UTF_8));
    }

    @JsonCreator
    public TDColumn(
            @JsonProperty("name") String name,
            @JsonProperty("type") String type,
            @JsonProperty("key") String key)
    {
        this(name, TDColumnType.parseColumnType(type), key.getBytes(UTF_8));
    }

    public TDColumn(String name, TDColumnType type, byte[] key)
    {
        this.name = checkNotNull(name, "name is null");
        this.type = checkNotNull(type, "type is null");
        this.key = Arrays.copyOf(checkNotNull(key, "key is null"), key.length);
    }

    @JsonProperty
    public String getName()
    {
        return name;
    }

    @JsonProperty
    public TDColumnType getType()
    {
        return type;
    }

    @JsonIgnore
    public byte[] getKey()
    {
        return Arrays.copyOf(key, key.length);
    }

    @JsonProperty("key")
    public String getKeyString()
    {
        return new String(key, StandardCharsets.UTF_8);
    }

    private static JSONArray castToArray(Object obj)
    {
        if (obj instanceof JSONArray) {
            return (JSONArray) obj;
        }
        else {
            throw new RuntimeJsonMappingException("Not an json array: " + obj);
        }
    }

    public static List<TDColumn> parseTuple(String jsonStr)
    {
        // unescape json quotation
        try {
            String unescaped = jsonStr.replaceAll("\\\"", "\"");
            JSONArray arr = castToArray(new JSONParser().parse(unescaped));
            List<TDColumn> columnList = new ArrayList<TDColumn>(arr.size());
            for (Object e : arr) {
                JSONArray columnNameAndType = castToArray(e);
                String[] s = new String[columnNameAndType.size()];
                for (int i = 0; i < columnNameAndType.size(); ++i) {
                    s[i] = columnNameAndType.get(i).toString();
                }
                columnList.add(parseTuple(s));
            }
            return columnList;
        }
        catch (ParseException e) {
            LoggerFactory.getLogger(TDColumn.class).error("Failed to parse json string", e);
            return new ArrayList<TDColumn>(0);
        }
    }

    public static TDColumn parseTuple(String[] tuple)
    {
        // TODO encode key in some ways
        if (tuple != null) {
            if (tuple.length == 2) {
                // [ key, type ]
                return new TDColumn(
                        tuple[0],
                        TDColumnType.parseColumnType(tuple[1]),
                        tuple[0].getBytes(StandardCharsets.UTF_8));
            }
            else if (tuple.length == 3) {
                // [ key, type, name ]
                return new TDColumn(
                        tuple[2],
                        TDColumnType.parseColumnType(tuple[1]),
                        tuple[0].getBytes(StandardCharsets.UTF_8));
            }
        }
        throw new RuntimeJsonMappingException("Unexpected string tuple to deserialize TDColumn");
    }

    @JsonIgnore
    public boolean isPartitionKey()
    {
        return Arrays.equals(LOG_TABLE_PUSHDOWN_KEY, getKey());
    }

    @JsonIgnore
    public String[] getTuple()
    {
        return new String[] {getKeyString(), type.toString(), name};
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        TDColumn other = (TDColumn) obj;
        return Objects.equal(this.name, other.name) &&
                Objects.equal(type, other.type) &&
                Arrays.equals(key, other.key);
    }

    @Override
    public int hashCode()
    {
        return Objects.hashCode(name, type, Arrays.hashCode(key));
    }

    @Override
    public String toString()
    {
        return String.format("%s:%s", name, type.toString());
    }
} public class TDConnectionLookupResult
{
    private final long id;

    public TDConnectionLookupResult(@JsonProperty("id") long id)
    {
        this.id = id;
    }

    public long getId()
    {
        return id;
    }

    @Override
    public String toString()
    {
        return "TDConnectionLookupResult{" +
                "id='" + id + '\'' +
                '}';
    }
} @JsonCollectionRootName(value = "databases")
public class TDDatabase
{
    private final String name;
    private final long count;
    private final String createdAt;
    private final String updatedAt;
    private final Optional<String> organization;
    private final String permission;

    @JsonCreator
    public TDDatabase(
            @JsonProperty("name") String name,
            @JsonProperty("count") long count,
            @JsonProperty("created_at") String createdAt,
            @JsonProperty("updated_at") String updatedAt,
            @JsonProperty("organization") Optional<String> organization,
            @JsonProperty("permission") String permission
    )
    {
        this.name = name;
        this.count = count;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.organization = organization;
        this.permission = permission;
    }

    @JsonProperty
    public String getName()
    {
        return name;
    }

    /**
     * Record count
     *
     * @return
     */
    @JsonProperty
    public long getCount()
    {
        return count;
    }

    @JsonProperty
    public String getCreatedAt()
    {
        return createdAt;
    }

    @JsonProperty
    public String getUpdatedAt()
    {
        return updatedAt;
    }

    @JsonProperty
    public Optional<String> getOrganization()
    {
        return organization;
    }

    @JsonProperty
    public String getPermission()
    {
        return permission;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        TDDatabase other = (TDDatabase) obj;
        return Objects.equal(this.name, other.name);
    }

    @Override
    public int hashCode()
    {
        return Objects.hashCode(name);
    }
} @Value.Style(typeBuilder = "TDExportJobRequestBuilder")
public class TDExportJobRequest
{
    private final String database;
    private final String table;
    private final Date from;
    private final Date to;
    private final TDExportFileFormatType fileFormat;
    private final String accessKeyId;
    private final String secretAccessKey;
    private final String bucketName;
    private final String filePrefix;
    private final Optional<String> poolName;
    private final Optional<String> domainKey;

    /**
     * @deprecated Use {@link #builder()} instead.
     */
    @Deprecated
    public TDExportJobRequest(
            String database,
            String table,
            Date from,
            Date to,
            TDExportFileFormatType fileFormat,
            String accessKeyId,
            String secretAccessKey,
            String bucketName,
            String filePrefix,
            Optional<String> poolName)
    {
        this.database = database;
        this.table = table;
        this.from = (Date) from.clone();
        this.to = (Date) to.clone();
        this.fileFormat = fileFormat;
        this.accessKeyId = accessKeyId;
        this.secretAccessKey = secretAccessKey;
        this.bucketName = bucketName;
        this.filePrefix = filePrefix;
        this.poolName = poolName;
        this.domainKey = Optional.absent();
    }

    private TDExportJobRequest(String database, String table, Date from, Date to, TDExportFileFormatType fileFormat, String accessKeyId, String secretAccessKey, String bucketName, String filePrefix, Optional<String> poolName, Optional<String> domainKey)
    {
        this.database = database;
        this.table = table;
        this.from = from;
        this.to = to;
        this.fileFormat = fileFormat;
        this.accessKeyId = accessKeyId;
        this.secretAccessKey = secretAccessKey;
        this.bucketName = bucketName;
        this.filePrefix = filePrefix;
        this.poolName = poolName;
        this.domainKey = domainKey;
    }

    public String getDatabase()
    {
        return database;
    }

    public String getTable()
    {
        return table;
    }

    public Date getFrom()
    {
        // Date object is mutable
        return new Date(from.getTime());
    }

    public Date getTo()
    {
        // Date object is mutable
        return new Date(to.getTime());
    }

    public TDExportFileFormatType getFileFormat()
    {
        return fileFormat;
    }

    public String getAccessKeyId()
    {
        return accessKeyId;
    }

    public String getSecretAccessKey()
    {
        return secretAccessKey;
    }

    public String getBucketName()
    {
        return bucketName;
    }

    public String getFilePrefix()
    {
        return filePrefix;
    }

    public Optional<String> getPoolName()
    {
        return poolName;
    }

    public Optional<String> getDomainKey()
    {
        return domainKey;
    }

    @Builder.Factory
    static TDExportJobRequest of(String database, String table, Date from, Date to, TDExportFileFormatType fileFormat, String accessKeyId, String secretAccessKey, String bucketName, String filePrefix, Optional<String> poolName, Optional<String> domainKey)
    {
        return new TDExportJobRequest(database, table, from, to, fileFormat, accessKeyId, secretAccessKey, bucketName, filePrefix, poolName, domainKey);
    }

    public static TDExportJobRequestBuilder builder()
    {
        return new TDExportJobRequestBuilder();
    }

    @Override
    public String toString()
    {
        return "TDExportJobRequest{" +
                "database='" + database + '\'' +
                ", table='" + table + '\'' +
                ", from=" + (from.getTime() / 1000) +
                ", to=" + (to.getTime() / 1000) +
                ", fileFormat='" + fileFormat + '\'' +
                ", accessKeyId='" + accessKeyId + '\'' +
                ", bucketName='" + bucketName + '\'' +
                ", filePrefix='" + filePrefix + '\'' +
                '}';
    }
} @Value.Style(typeBuilder = "TDExportResultJobRequestBuilder")
public class TDExportResultJobRequest
{
    private final String jobId;
    private final String resultOutput;
    private final String resultConnectionId;
    private final String resultConnectionSettings;

    private TDExportResultJobRequest(String jobId, String resultOutput, String reseultConnectionId, String resultConnectionSettings)
    {
        this.jobId = jobId;
        this.resultOutput = resultOutput;
        this.resultConnectionId = reseultConnectionId;
        this.resultConnectionSettings = resultConnectionSettings;
    }

    public String getJobId()
    {
        return jobId;
    }

    public String getResultOutput()
    {
        return resultOutput;
    }

    public String getResultConnectionId()
    {
        return resultConnectionId;
    }

    public String getResultConnectionSettings()
    {
        return resultConnectionSettings;
    }

    @Builder.Factory
    static TDExportResultJobRequest of(String jobId,
            Optional<String> resultOutput,
            Optional<String> resultConnectionId,
            Optional<String> resultConnectionSettings)
    {
        return new TDExportResultJobRequest(jobId,
                resultOutput.or(""),
                resultConnectionId.or(""),
                resultConnectionSettings.or(""));
    }

    public static TDExportResultJobRequestBuilder builder()
    {
        return new TDExportResultJobRequestBuilder();
    }

    @Override
    public String toString()
    {
        return "TDExportResultJobRequest{" +
                "jobId='" + jobId + '\'' +
                ", resultOutput='" + resultOutput + '\'' +
                ", resultConnectionId='" + resultConnectionId + '\'' +
                ", resultConnectionSettings='" + resultConnectionSettings + '\'' +
                '}';
    }
} public class TDImportResult
{
    private final String databaseName;
    private final String tableName;
    private final String uniqueId;
    private final long elapsedTime;
    private final String md5Hex;

    @JsonCreator
    public TDImportResult(
            @JsonProperty("database") String databaseName,
            @JsonProperty("table") String tableName,
            @JsonProperty("elapsed_time") long elapsedTime,
            @JsonProperty("unique_id") String uniqueId,
            @JsonProperty("md5_hex") String md5Hex)
    {
        this.databaseName = databaseName;
        this.tableName = tableName;
        this.elapsedTime = elapsedTime;
        this.uniqueId = uniqueId;
        this.md5Hex = md5Hex;
    }

    @JsonProperty("database")
    public String getDatabaseName()
    {
        return databaseName;
    }

    @JsonProperty("table")
    public String getTableName()
    {
        return tableName;
    }

    @JsonProperty("unique_id")
    public String getUniqueId()
    {
        return uniqueId;
    }

    @JsonProperty("elapsed_time")
    public long getElapsedTime()
    {
        return elapsedTime;
    }

    @JsonProperty("md5_hex")
    public String getMd5Hex()
    {
        return md5Hex;
    }
} public class TDJob
{
    public static enum Type
    {
        HIVE("hive"), MAPRED("mapred"), PRESTO("presto"), PIG("pig"), BULKLOAD("bulkload"), EXPORT("export"), UNKNOWN("none");
        private final String type;

        private Type(String type)
        {
            this.type = type;
        }

        @JsonValue
        public String getType()
        {
            return type;
        }

        @Override
        public String toString()
        {
            return type;
        }

        @JsonCreator
        public static Type fromString(String typeName)
        {
            for (Type t : values()) {
                if (t.type.equals(typeName)) {
                    return t;
                }
            }
            return UNKNOWN;
        }
    }

    public static enum Priority
    {
        VERYLOW(-2), LOW(-1), NORMAL(0), HIGH(1), VERYHIGH(2);
        private final int priority;

        private Priority(int priority)
        {
            this.priority = priority;
        }

        public int toInt()
        {
            return priority;
        }

        public static Priority fromInt(int priority)
        {
            for (Priority p : values()) {
                if (p.priority == priority) {
                    return p;
                }
            }
            // For unknown property value, returns the default value NORMAL
            return NORMAL;
        }
    }

    public static enum Status
    {
        QUEUED, BOOTING, RUNNING, SUCCESS, ERROR, KILLED, UNKNOWN;

        @JsonCreator
        public static Status fromString(String s)
        {
            return valueOf(s.toUpperCase());
        }

        public boolean isFinished()
        {
            return this == SUCCESS ||
                    this == ERROR ||
                    this == KILLED;
        }
    }

    /**
     * Engine version for hive, presto
     * To accept specific version number like 'Hive TD 2018.01', Implement as not enum but class
     */
    public static class EngineVersion
    {
        private final String engineVersion;

        private EngineVersion(String engineVersion)
        {
            this.engineVersion = engineVersion;
        }

        @JsonValue
        public String getEngineVersion()
        {
            return engineVersion;
        }

        @Override
        public String toString()
        {
            return engineVersion;
        }

        public static EngineVersion fromString(String engineVersion)
        {
            return new EngineVersion(engineVersion);
        }
    }

    private static class Debug
    {
        private final Optional<String> cmdout;
        private final Optional<String> stderr;

        @JsonCreator
        public Debug(@JsonProperty("cmdout") Optional<String> cmdout, @JsonProperty("stderr") Optional<String> stderr)
        {
            this.cmdout = cmdout;
            this.stderr = stderr;
        }

        public String getCmdout()
        {
            return cmdout.or("");
        }

        public String getStderr()
        {
            return stderr.or("");
        }

        @Override
        public String toString()
        {
            return "Debug{" +
                    "cmdout='" + cmdout + '\'' +
                    ", stderr='" + stderr + '\'' +
                    '}';
        }
    }

    private final String jobId;
    private final Status status;
    private final Type type;
    private final String query;
    private final String createdAt;
    private final String startAt;
    private final String updatedAt;
    private final String endAt;
    private final Optional<String> resultSchema;  // only for Hive
    private final String database;
    private final String result;
    private final String url;
    private final String userName;
    private final long duration;
    private final long resultSize;
    private final Optional<Debug> debug;
    private final long numRecords;
    private final Optional<EngineVersion> engineVersion;

    @JsonCreator
    static TDJob createTDJobV3(
            @JsonProperty("job_id") String jobId,
            @JsonProperty("status") Status status,
            @JsonProperty("type") Type type,
            @JsonProperty("query") TDQuery query,
            @JsonProperty("created_at") String createdAt,
            @JsonProperty("start_at") String startAt,
            @JsonProperty("updated_at") String updatedAt,
            @JsonProperty("end_at") String endAt,
            @JsonProperty("hive_result_schema") Optional<String> resultSchema,
            @JsonProperty("database") String database,
            @JsonProperty("result") String result,
            @JsonProperty("url") String url,
            @JsonProperty("user_name") String userName,
            @JsonProperty("duration") long duration,
            @JsonProperty("result_size") long resultSize,
            @JsonProperty("debug") Optional<Debug> debug,
            @JsonProperty("num_records") long numRecords,
            @JsonProperty("engine_version") Optional<EngineVersion> engineVersion)
    {
        return new TDJob(jobId, status, type, query.getQuery(), createdAt, startAt, updatedAt, endAt, resultSchema, database, result, url, userName, duration, resultSize, debug, numRecords, engineVersion);
    }

    public TDJob(String jobId,
            Status status,
            Type type,
            String query,
            String createdAt,
            String startAt,
            String updatedAt,
            String endAt,
            Optional<String> resultSchema,
            String database,
            String result,
            String url,
            String userName,
            long duration,
            long resultSize,
            Optional<Debug> debug,
            long numRecords,
            Optional<EngineVersion> engineVersion
    )
    {
        this.jobId = jobId;
        this.status = status;
        this.type = type;
        this.query = query;
        this.createdAt = createdAt;
        this.startAt = startAt;
        this.updatedAt = updatedAt;
        this.endAt = endAt;
        this.resultSchema = resultSchema;
        this.database = database;
        this.result = result;
        this.url = url;
        this.userName = userName;
        this.duration = duration;
        this.resultSize = resultSize;
        this.debug = debug;
        this.numRecords = numRecords;
        this.engineVersion = engineVersion;
    }

    public String getJobId()
    {
        return jobId;
    }

    public Status getStatus()
    {
        return status;
    }

    public Type getType()
    {
        return type;
    }

    public String getQuery()
    {
        return query;
    }

    public String getCreatedAt()
    {
        return createdAt;
    }

    public String getStartAt()
    {
        return startAt;
    }

    public String getEndAt()
    {
        return endAt;
    }

    public Optional<String> getResultSchema()
    {
        return resultSchema;
    }

    public String getDatabase()
    {
        return database;
    }

    public String getResult()
    {
        return result;
    }

    public String getUrl()
    {
        return url;
    }

    public String getUserName()
    {
        return userName;
    }

    public long getDuration()
    {
        return duration;
    }

    public long getResultSize()
    {
        return resultSize;
    }

    public Optional<Debug> getDebug()
    {
        return debug;
    }

    public long getNumRecords()
    {
        return numRecords;
    }

    public Optional<EngineVersion> getEngineVersion()
    {
        return engineVersion;
    }

    /**
     * A short cut for reading cmdout message
     *
     * @return
     */
    public String getCmdOut()
    {
        return debug.transform(new Function<Debug, String>()
        {
            @Override
            public String apply(Debug input)
            {
                return input.getCmdout();
            }
        }).or("");
    }

    /**
     * A short cut for reading stderr messsage
     *
     * @return
     */
    public String getStdErr()
    {
        return debug.transform(new Function<Debug, String>()
        {
            @Override
            public String apply(Debug input)
            {
                return input.getStderr();
            }
        }).or("");
    }

    @Override
    public String toString()
    {
        return "TDJob{" +
                "jobId='" + jobId + '\'' +
                ", status=" + status +
                ", type=" + type +
                ", query=" + query +
                ", createdAt='" + createdAt + '\'' +
                ", startAt='" + startAt + '\'' +
                ", updatedAt='" + updatedAt + '\'' +
                ", endAt='" + endAt + '\'' +
                ", resultSchema=" + resultSchema +
                ", database='" + database + '\'' +
                ", result='" + result + '\'' +
                ", url='" + url + '\'' +
                ", userName='" + userName + '\'' +
                ", duration=" + duration +
                ", resultSize=" + resultSize +
                ", debug=" + debug +
                ", numRecords=" + numRecords +
                ", engineVersion=" + engineVersion +
                '}';
    }
} public class TDJobList
{
    private final List<TDJob> jobs;
    private final int count;
    private final Optional<Long> from;
    private final Optional<Long> to;

    public TDJobList(
            @JsonProperty("jobs") List<TDJob> jobs,
            @JsonProperty("count") int count,
            @JsonProperty("from") Optional<Long> from,
            @JsonProperty("to") Optional<Long> to)
    {
        this.jobs = jobs;
        this.count = count;
        this.from = from;
        this.to = to;
    }

    public List<TDJob> getJobs()
    {
        return jobs;
    }

    public int getCount()
    {
        return count;
    }

    public Optional<Long> getFrom()
    {
        return from;
    }

    public Optional<Long> getTo()
    {
        return to;
    }

    @Override
    public String toString()
    {
        return Joiner.on("\n").join(jobs);
    }
} public class TDJobRequest
{
    private final String database;
    private final TDJob.Type type;
    private final String query;
    private final TDJob.Priority priority;
    private final Optional<String> resultOutput;
    private final Optional<Integer> retryLimit;
    private final Optional<String> poolName;
    private final Optional<String> table;
    private final Optional<ObjectNode> config;
    private final Optional<Long> scheduledTime;
    private final Optional<String> domainKey;
    private final Optional<Long> resultConnectionId;
    private final Optional<String> resultConnectionSettings;
    private final Optional<TDJob.EngineVersion> engineVersion;

    @Deprecated
    public TDJobRequest(String database, TDJob.Type type, String query, TDJob.Priority priority, Optional<String> resultOutput, Optional<Integer> retryLimit, Optional<String> poolName, Optional<String> table, Optional<ObjectNode> config)
    {
        this.database = database;
        this.type = type;
        this.query = query;
        this.priority = priority;
        this.resultOutput = resultOutput;
        this.retryLimit = retryLimit;
        this.poolName = poolName;
        this.table = table;
        this.config = config;
        this.scheduledTime = Optional.absent();
        this.domainKey = Optional.absent();
        this.resultConnectionId = Optional.absent();
        this.resultConnectionSettings = Optional.absent();
        this.engineVersion = Optional.absent();
    }

    private TDJobRequest(TDJobRequestBuilder builder)
    {
        this.database = builder.getDatabase();
        this.type = builder.getType();
        this.query = builder.getQuery();
        this.priority = builder.getPriority();
        this.resultOutput = builder.getResultOutput();
        this.retryLimit = builder.getRetryLimit();
        this.poolName = builder.getPoolName();
        this.table = builder.getTable();
        this.config = builder.getConfig();
        this.scheduledTime = builder.getScheduledTime();
        this.domainKey = builder.getDomainKey();
        this.resultConnectionId = builder.getResultConnectionId();
        this.resultConnectionSettings = builder.getResultConnectionSettings();
        this.engineVersion = builder.getEngineVersion();

    }

    public static TDJobRequest newPrestoQuery(String database, String query)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.PRESTO)
                .setDatabase(database)
                .setQuery(query)
                .createTDJobRequest();
    }

    public static TDJobRequest newPrestoQuery(String database, String query, String resultOutput)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.PRESTO)
                .setDatabase(database)
                .setQuery(query)
                .setResultOutput(resultOutput)
                .createTDJobRequest();
    }

    public static TDJobRequest newPrestoQuery(String database, String query, String resultOutput, String poolName)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.PRESTO)
                .setDatabase(database)
                .setQuery(query)
                .setResultOutput(resultOutput)
                .setPoolName(poolName)
                .createTDJobRequest();
    }

    public static TDJobRequest newHiveQuery(String database, String query)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.HIVE)
                .setDatabase(database)
                .setQuery(query)
                .createTDJobRequest();
    }

    public static TDJobRequest newHiveQuery(String database, String query, String resultOutput)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.HIVE)
                .setDatabase(database)
                .setQuery(query)
                .setResultOutput(resultOutput)
                .createTDJobRequest();
    }

    public static TDJobRequest newHiveQuery(String database, String query, String resultOutput, String poolName)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.HIVE)
                .setDatabase(database)
                .setQuery(query)
                .setResultOutput(resultOutput)
                .setPoolName(poolName)
                .createTDJobRequest();
    }

    public static TDJobRequest newPigQuery(String database, String query)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.PIG)
                .setDatabase(database)
                .setQuery(query)
                .createTDJobRequest();
    }

    public static TDJobRequest newPigQuery(String database, String query, String resultOutput)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.PIG)
                .setDatabase(database)
                .setQuery(query)
                .setResultOutput(resultOutput)
                .createTDJobRequest();
    }

    public static TDJobRequest newPigQuery(String database, String query, String resultOutput, String poolName)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.PIG)
                .setDatabase(database)
                .setQuery(query)
                .setResultOutput(resultOutput)
                .setPoolName(poolName)
                .createTDJobRequest();
    }

    public static TDJobRequest newBulkLoad(String database, String table, ObjectNode config)
    {
        return new TDJobRequestBuilder()
                .setType(TDJob.Type.BULKLOAD)
                .setDatabase(database)
                .setTable(table)
                .setConfig(config)
                .setQuery("")
                .createTDJobRequest();
    }

    public String getDatabase()
    {
        return database;
    }

    public Optional<String> getTable()
    {
        return table;
    }

    public TDJob.Type getType()
    {
        return type;
    }

    public String getQuery()
    {
        return query;
    }

    public TDJob.Priority getPriority()
    {
        return priority;
    }

    public Optional<Integer> getRetryLimit()
    {
        return retryLimit;
    }

    public Optional<String> getResultOutput()
    {
        return resultOutput;
    }

    public Optional<String> getPoolName()
    {
        return poolName;
    }

    public Optional<ObjectNode> getConfig()
    {
        return config;
    }

    public Optional<Long> getScheduledTime()
    {
        return scheduledTime;
    }

    public Optional<String> getDomainKey()
    {
        return domainKey;
    }

    public Optional<Long> getResultConnectionId()
    {
        return resultConnectionId;
    }

    public Optional<String> getResultConnectionSettings()
    {
        return resultConnectionSettings;
    }

    public Optional<TDJob.EngineVersion> getEngineVersion()
    {
        return engineVersion;
    }

    static TDJobRequest of(TDJobRequestBuilder builder)
    {
        return new TDJobRequest(builder);
    }

    @Override
    public String toString()
    {
        return "TDJobRequest{" +
                "database='" + database + '\'' +
                ", type=" + type +
                ", query='" + query + '\'' +
                ", priority=" + priority +
                ", resultOutput=" + resultOutput +
                ", retryLimit=" + retryLimit +
                ", poolName=" + poolName +
                ", table=" + table +
                ", config=" + config +
                ", scheduledTime=" + scheduledTime +
                ", domainKey=" + domainKey +
                ", resultConnectionId=" + resultConnectionId +
                ", resultConnectionSettings=" + resultConnectionSettings +
                ", engineVersion=" + engineVersion +
                '}';
    }
} public class TDJobSubmitResult
{
    private final String jobId;

    public TDJobSubmitResult(@JsonProperty("job_id") String jobId)
    {
        this.jobId = jobId;
    }

    public String getJobId()
    {
        return jobId;
    }

    @Override
    public String toString()
    {
        return "TDJobStatus{" +
                "jobId='" + jobId + '\'' +
                '}';
    }
} public class TDJobSummary
{
    private final TDJob.Status status;
    private final long resultSize;
    private final long duration;
    private final String jobId;
    private final String createdAt;
    private final String updatedAt;
    private final String startedAt;
    private final String endAt;

    @JsonCreator
    public TDJobSummary(
            @JsonProperty("status") TDJob.Status status,
            @JsonProperty("result_size") long resultSize,
            @JsonProperty("duration") long duration,
            @JsonProperty("job_id") String jobId,
            @JsonProperty("created_at") String createdAt,
            @JsonProperty("updated_at") String updatedAt,
            @JsonProperty("started_at") String startedAt,
            @JsonProperty("end_at") String endAt)
    {
        this.status = status;
        this.resultSize = resultSize;
        this.duration = duration;
        this.jobId = jobId;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.startedAt = startedAt;
        this.endAt = endAt;
    }

    public TDJob.Status getStatus()
    {
        return status;
    }

    public long getResultSize()
    {
        return resultSize;
    }

    public long getDuration()
    {
        return duration;
    }

    public String getJobId()
    {
        return jobId;
    }

    public String getCreatedAt()
    {
        return createdAt;
    }

    public String getUpdatedAt()
    {
        return updatedAt;
    }

    public String getStartedAt()
    {
        return startedAt;
    }

    public String getEndAt()
    {
        return endAt;
    }

    @Override
    public String toString()
    {
        return "TDJobStatus{" +
                "status=" + status +
                ", resultSize=" + resultSize +
                ", duration=" + duration +
                ", jobId='" + jobId + '\'' +
                ", createdAt='" + createdAt + '\'' +
                ", updatedAt='" + updatedAt + '\'' +
                ", startedAt='" + startedAt + '\'' +
                ", endAt='" + endAt + '\'' +
                '}';
    }
} public class TDPartialDeleteJob
{
    private final String jobId;
    private final String database;
    private final String table;
    private final long from;
    private final long to;

    public TDPartialDeleteJob(
            @JsonProperty("job_id") String jobId,
            @JsonProperty("database") String database,
            @JsonProperty("table") String table,
            @JsonProperty("long") long from,
            @JsonProperty("to") long to)
    {
        this.jobId = jobId;
        this.database = database;
        this.table = table;
        this.from = from;
        this.to = to;
    }

    public String getJobId()
    {
        return jobId;
    }

    public String getDatabase()
    {
        return database;
    }

    public String getTable()
    {
        return table;
    }

    public long getFrom()
    {
        return from;
    }

    public long getTo()
    {
        return to;
    }

    @Override
    public String toString()
    {
        return "TDPartialDeleteJob{" +
                "jobId='" + jobId + '\'' +
                ", database='" + database + '\'' +
                ", table='" + table + '\'' +
                ", from=" + from +
                ", to=" + to +
                '}';
    }
} public enum TDResultFormat
{
    JSON("json"),
    TSV("tsv"),
    CSV("csv"),
    MESSAGE_PACK("msgpack"),
    MESSAGE_PACK_GZ("msgpack.gz");
    private final String name;

    private TDResultFormat(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }
} public class TDSaveQueryRequest
{
    private final String name;
    private final String cron;
    private final TDJob.Type type;
    private final String query;
    private final String timezone;
    private final long delay;
    private final String database;
    private final int priority;
    private final int retryLimit;
    private final String result;
    private final TDJob.EngineVersion engineVersion;

    public TDSaveQueryRequest(
            @JsonProperty("name") String name,
            @JsonProperty("cron") String cron,
            @JsonProperty("type") TDJob.Type type,
            @JsonProperty("query") String query,
            @JsonProperty("timezone") String timezone,
            @JsonProperty("delay") long delay,
            @JsonProperty("database") String database,
            @JsonProperty("priority") int priority,
            @JsonProperty("retry_limit") int retryLimit,
            @JsonProperty("result") String result,
            @JsonProperty("engine_version") @JsonInclude(JsonInclude.Include.NON_NULL) TDJob.EngineVersion engineVersion)
    {
        this.name = name;
        this.cron = cron;
        this.type = type;
        this.query = query;
        this.timezone = timezone;
        this.delay = delay;
        this.database = database;
        this.priority = priority;
        this.retryLimit = retryLimit;
        this.result = result;
        this.engineVersion = engineVersion;
    }

    public String getName()
    {
        return name;
    }

    public String getCron()
    {
        return cron;
    }

    public TDJob.Type getType()
    {
        return type;
    }

    public String getQuery()
    {
        return query;
    }

    public String getTimezone()
    {
        return timezone;
    }

    public long getDelay()
    {
        return delay;
    }

    public String getDatabase()
    {
        return database;
    }

    public int getPriority()
    {
        return priority;
    }

    @JsonProperty("retry_limit")
    public int getRetryLimit()
    {
        return retryLimit;
    }

    public String getResult()
    {
        return result;
    }

    @JsonProperty("engine_version")
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public TDJob.EngineVersion getEngineVersion()
    {
        return engineVersion;
    }
} @JsonCollectionRootName(value = "schedules")
public class TDSavedQuery
{
    public static TDSavedQueryBuilder newBuilder(
            String name,
            TDJob.Type type,
            String database,
            String query,
            String timezone)
    {
        return new TDSavedQueryBuilder(name).setType(type).setQuery(query).setTimezone(timezone).setDatabase(database);
    }

    public static TDSavedQueryUpdateRequestBuilder newUpdateRequestBuilder()
    {
        return new TDSavedQueryUpdateRequestBuilder();
    }

    public TDSavedQueryUpdateRequestBuilder toUpdateRequestBuilder()
    {
        TDSavedQueryUpdateRequestBuilder b = new TDSavedQueryUpdateRequestBuilder();
        b.setName(name);
        b.setCron(cron);
        b.setType(type);
        b.setQuery(query);
        b.setTimezone(timezone);
        b.setDelay(delay);
        b.setDatabase(database);
        b.setPriority(priority);
        b.setRetryLimit(retryLimit);
        b.setResult(result);
        b.setEngineVersion(engineVersion);
        return b;
    }

    private final String id;
    private final String name;
    private final String cron;
    private final TDJob.Type type;
    private final String query;
    private final String timezone;
    private final long delay;
    private final String database;
    private final String userName;
    private final int priority;
    private final int retryLimit;
    private final String result;
    private final String nextTime;
    private final TDJob.EngineVersion engineVersion;

    public TDSavedQuery(
            @JsonProperty("id") String id,
            @JsonProperty("name") String name,
            @JsonProperty("cron") String cron,
            @JsonProperty("type") TDJob.Type type,
            @JsonProperty("query") String query,
            @JsonProperty("timezone") String timezone,
            @JsonProperty("delay") long delay,
            @JsonProperty("database") String database,
            @JsonProperty("user_name") String userName,
            @JsonProperty("priority") int priority,
            @JsonProperty("retry_limit") int retryLimit,
            @JsonProperty("result") String result,
            @JsonProperty("next_time") String nextTime,
            @JsonProperty("engine_version") TDJob.EngineVersion engineVersion
            )
    {
        this.id = id;
        this.name = name;
        this.cron = cron;
        this.type = type;
        this.query = query;
        this.timezone = timezone;
        this.delay = delay;
        this.database = database;
        this.userName = userName;
        this.priority = priority;
        this.retryLimit = retryLimit;
        this.result = result;
        this.nextTime = nextTime;
        this.engineVersion = engineVersion;
    }

    public String getId()
    {
        return id;
    }

    public String getName()
    {
        return name;
    }

    public String getCron()
    {
        return cron;
    }

    public TDJob.Type getType()
    {
        return type;
    }

    public String getQuery()
    {
        return query;
    }

    public String getTimezone()
    {
        return timezone;
    }

    public long getDelay()
    {
        return delay;
    }

    public String getDatabase()
    {
        return database;
    }

    public String getUserName()
    {
        return userName;
    }

    public int getPriority()
    {
        return priority;
    }

    public int getRetryLimit()
    {
        return retryLimit;
    }

    /**
     * Get the result target URL. This returns an empty string if nothing is specified
     *
     * @return Get the result target URL
     */
    public String getResult()
    {
        return result;
    }

    public String getNextTime()
    {
        return nextTime;
    }

    public TDJob.EngineVersion getEngineVersion()
    {
        return engineVersion;
    }

    @Override
    public String toString()
    {
        return "TDSavedQuery{" +
                "name='" + name + '\'' +
                ", cron='" + cron + '\'' +
                ", type=" + type +
                ", query='" + query + '\'' +
                ", timezone='" + timezone + '\'' +
                ", delay=" + delay +
                ", database='" + database + '\'' +
                ", userName='" + userName + '\'' +
                ", priority=" + priority +
                ", retryLimit=" + retryLimit +
                ", result='" + result + '\'' +
                ", nextTime='" + nextTime + '\'' +
                ", engineVersion='" + engineVersion + '\'' +
                '}';
    }
} public class TDSavedQueryHistory
{
    private final List<TDJob> history;
    private final int count;
    private final Optional<Long> from;
    private final Optional<Long> to;

    TDSavedQueryHistory(
            @JsonProperty("history") List<TDJob> history,
            @JsonProperty("count") int count,
            @JsonProperty("from") Optional<Long> from,
            @JsonProperty("to") Optional<Long> to)
    {
        this.history = history;
        this.count = count;
        this.from = from;
        this.to = to;
    }

    public List<TDJob> getHistory()
    {
        return history;
    }

    public int getCount()
    {
        return count;
    }

    public Optional<Long> getFrom()
    {
        return from;
    }

    public Optional<Long> getTo()
    {
        return to;
    }

    @Override
    public String toString()
    {
        return "TDSavedQueryHistory{" +
                "history=" + history +
                ", count=" + count +
                ", from=" + from +
                ", to=" + to +
                '}';
    }
} @Value.Immutable
@Value.Style(visibility = Value.Style.ImplementationVisibility.PACKAGE)
@JsonSerialize(as = ImmutableTDSavedQueryStartRequest.class)
@JsonDeserialize(as = ImmutableTDSavedQueryStartRequest.class)
public abstract class TDSavedQueryStartRequest
{
    public abstract String name();
    public abstract Optional<Long> id();
    public abstract Date scheduledTime();
    public abstract Optional<Integer> num();
    public abstract Optional<String> domainKey();

    public interface Builder
    {
        Builder name(String name);
        Builder id(long id);
        Builder scheduledTime(Date scheduledTime);
        Builder num(Optional<Integer> num);
        Builder num(int num);
        Builder domainKey(Optional<String> domainKey);
        Builder domainKey(String domainKey);
        TDSavedQueryStartRequest build();
    }

    public static TDSavedQueryStartRequest.Builder builder()
    {
        return ImmutableTDSavedQueryStartRequest.builder();
    }
} @Value.Immutable
@Value.Style(visibility = Value.Style.ImplementationVisibility.PACKAGE)
@JsonSerialize(as = ImmutableTDSavedQueryStartRequestV4.class)
@JsonDeserialize(as = ImmutableTDSavedQueryStartRequestV4.class)
public abstract class TDSavedQueryStartRequestV4
{
    @JsonProperty("scheduled_time") public abstract String scheduledTime();
    @JsonProperty("domain_key") public abstract Optional<String> domainKey();

    public static TDSavedQueryStartRequestV4 from(TDSavedQueryStartRequest request)
    {
        TimeZone tz = TimeZone.getTimeZone("UTC");
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        df.setTimeZone(tz);

        return builder()
                .domainKey(request.domainKey())
                .scheduledTime(df.format(request.scheduledTime()))
                .build();
    }

    public interface Builder
    {
        Builder scheduledTime(String scheduledTime);
        Builder domainKey(Optional<String> domainKey);
        Builder domainKey(String domainKey);
        TDSavedQueryStartRequestV4 build();
    }

    public static TDSavedQueryStartRequestV4.Builder builder()
    {
        return ImmutableTDSavedQueryStartRequestV4.builder();
    }
} @Value.Immutable
@Value.Style(visibility = Value.Style.ImplementationVisibility.PACKAGE)
@JsonSerialize(as = ImmutableTDSavedQueryStartResultV4.class)
@JsonDeserialize(as = ImmutableTDSavedQueryStartResultV4.class)
public abstract class TDSavedQueryStartResultV4
{
    @JsonProperty("id") public abstract String getId();
} public class TDSavedQueryUpdateRequest
{
    private final Optional<String> name;
    private final Optional<String> cron;
    private final Optional<TDJob.Type> type;
    private final Optional<String> query;
    private final Optional<String> timezone;
    private final Optional<Long> delay;
    private final Optional<String> database;
    private final Optional<Integer> priority;
    private final Optional<Integer> retryLimit;
    private final Optional<String> result;
    private final Optional<TDJob.EngineVersion> engineVersion;

    @VisibleForTesting
    @JsonCreator
    TDSavedQueryUpdateRequest(
            @JsonProperty("name") Optional<String> name,
            @JsonProperty("cron") Optional<String> cron,
            @JsonProperty("type") Optional<TDJob.Type> type,
            @JsonProperty("query") Optional<String> query,
            @JsonProperty("timezone") Optional<String> timezone,
            @JsonProperty("delay") Optional<Long> delay,
            @JsonProperty("database") Optional<String> database,
            @JsonProperty("priority") Optional<Integer> priority,
            @JsonProperty("retry_limit") Optional<Integer> retryLimit,
            @JsonProperty("result") Optional<String> result,
            @JsonProperty("engine_version") Optional<TDJob.EngineVersion> engineVersion)
    {
        this.name = name;
        this.cron = cron;
        this.type = type;
        this.query = query;
        this.timezone = timezone;
        this.delay = delay;
        this.database = database;
        this.priority = priority;
        this.retryLimit = retryLimit;
        this.result = result;
        this.engineVersion = engineVersion;
    }

    @VisibleForTesting
    static ObjectMapper getObjectMapper()
    {
        ObjectMapper mapper = new ObjectMapper();
        // Configure object mapper to exclude Optional.absent values in the generated json string
        mapper.registerModule(new GuavaModule().configureAbsentsAsNulls(false));
        mapper.setSerializationInclusion(JsonInclude.Include.NON_ABSENT);
        return mapper;
    }

    public String toJson()
    {
        try {
            return getObjectMapper().writeValueAsString(this);
        }
        catch (JsonProcessingException e) {
            // This should not happen in general
            throw new IllegalStateException(String.format("Failed to produce json data of TDSavedQueryUpdateRequest: %s", this), e);
        }
    }

    /**
     * Apply this update to the given TDSaveQuery objects and create a new TDSaveQueryRequest object.
     *
     * @param base
     * @return
     */
    @VisibleForTesting
    public TDSaveQueryRequest merge(TDSavedQuery base)
    {
        return new TDSaveQueryRequest(
                name.or(base.getName()),
                cron.or(base.getCron()),
                type.or(base.getType()),
                query.or(base.getQuery()),
                timezone.or(base.getTimezone()),
                delay.or(base.getDelay()),
                database.or(base.getDatabase()),
                priority.or(base.getPriority()),
                retryLimit.or(base.getRetryLimit()),
                result.or(base.getResult()),
                engineVersion.isPresent() ? engineVersion.get() : base.getEngineVersion());
    }

    @JsonProperty
    public Optional<String> getName()
    {
        return name;
    }

    @JsonProperty
    public Optional<String> getCron()
    {
        return cron;
    }

    @JsonProperty
    public Optional<TDJob.Type> getType()
    {
        return type;
    }

    @JsonProperty
    public Optional<String> getQuery()
    {
        return query;
    }

    @JsonProperty
    public Optional<String> getTimezone()
    {
        return timezone;
    }

    @JsonProperty
    public Optional<Long> getDelay()
    {
        return delay;
    }

    @JsonProperty
    public Optional<String> getDatabase()
    {
        return database;
    }

    @JsonProperty
    public Optional<Integer> getPriority()
    {
        return priority;
    }

    @JsonProperty("retry_limit")
    public Optional<Integer> getRetryLimit()
    {
        return retryLimit;
    }

    @JsonProperty
    public Optional<String> getResult()
    {
        return result;
    }

    @JsonProperty("engine_version")
    public Optional<TDJob.EngineVersion> getEngineVersion()
    {
        return engineVersion;
    }

    @Override
    public String toString()
    {
        return "TDSavedQueryUpdateRequest{" +
                "name=" + name +
                ", cron=" + cron +
                ", type=" + type +
                ", query=" + query +
                ", timezone=" + timezone +
                ", delay=" + delay +
                ", database=" + database +
                ", priority=" + priority +
                ", retryLimit=" + retryLimit +
                ", result=" + result +
                ", engineVersion=" + engineVersion +
                '}';
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        TDSavedQueryUpdateRequest that = (TDSavedQueryUpdateRequest) o;

        if (name != null ? !name.equals(that.name) : that.name != null) {
            return false;
        }
        if (cron != null ? !cron.equals(that.cron) : that.cron != null) {
            return false;
        }
        if (type != null ? !type.equals(that.type) : that.type != null) {
            return false;
        }
        if (query != null ? !query.equals(that.query) : that.query != null) {
            return false;
        }
        if (timezone != null ? !timezone.equals(that.timezone) : that.timezone != null) {
            return false;
        }
        if (delay != null ? !delay.equals(that.delay) : that.delay != null) {
            return false;
        }
        if (database != null ? !database.equals(that.database) : that.database != null) {
            return false;
        }
        if (priority != null ? !priority.equals(that.priority) : that.priority != null) {
            return false;
        }
        if (retryLimit != null ? !retryLimit.equals(that.retryLimit) : that.retryLimit != null) {
            return false;
        }
        if (engineVersion != null ? !engineVersion.equals(that.engineVersion) : that.engineVersion != null) {
            return false;
        }
        return result != null ? result.equals(that.result) : that.result == null;
    }

    @Override
    public int hashCode()
    {
        int result1 = name != null ? name.hashCode() : 0;
        result1 = 31 * result1 + (cron != null ? cron.hashCode() : 0);
        result1 = 31 * result1 + (type != null ? type.hashCode() : 0);
        result1 = 31 * result1 + (query != null ? query.hashCode() : 0);
        result1 = 31 * result1 + (timezone != null ? timezone.hashCode() : 0);
        result1 = 31 * result1 + (delay != null ? delay.hashCode() : 0);
        result1 = 31 * result1 + (database != null ? database.hashCode() : 0);
        result1 = 31 * result1 + (priority != null ? priority.hashCode() : 0);
        result1 = 31 * result1 + (retryLimit != null ? retryLimit.hashCode() : 0);
        result1 = 31 * result1 + (result != null ? result.hashCode() : 0);
        result1 = 31 * result1 + (engineVersion != null ? engineVersion.hashCode() : 0);
        return result1;
    }
} public class TDTable
{
    private final String id;
    private final String name;
    private final TDTableType type;
    private final List<TDColumn> columns;
    private final long rowCount;
    private final long estimatedStorageSize;
    private final String lastLogTimeStamp;
    private final String expireDays;
    private final String createdAt;
    private final String updatedAt;

    @JsonCreator
    public TDTable(
            @JsonProperty("id") String id,
            @JsonProperty("name") String name,
            @JsonProperty("type") TDTableType type,
            @JsonProperty("schema") String schema,
            @JsonProperty("count") long rowCount,
            @JsonProperty("estimated_storage_size") long estimatedStroageSize,
            @JsonProperty("last_log_timestamp") String lastLogTimeStamp,
            @JsonProperty("expire_days") String expireDays,
            @JsonProperty("created_at") String createdAt,
            @JsonProperty("updated_at") String updatedAt
    )
    {
        this.id = id;
        this.name = name;
        this.type = type;
        this.columns = TDColumn.parseTuple(schema);
        this.rowCount = rowCount;
        this.estimatedStorageSize = estimatedStroageSize;
        this.lastLogTimeStamp = lastLogTimeStamp;
        this.expireDays = expireDays;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }

    public String getId()
    {
        return id;
    }

    @JsonProperty
    public String getName()
    {
        return name;
    }

    @JsonProperty
    public TDTableType getType()
    {
        return type;
    }

    @JsonProperty
    public List<TDColumn> getColumns()
    {
        return columns;
    }

    /**
     * Alias to getColumns
     * @return
     */
    public List<TDColumn> getSchema()
    {
        return getColumns();
    }

    public long getRowCount()
    {
        return rowCount;
    }

    public long getEstimatedStorageSize()
    {
        return estimatedStorageSize;
    }

    public String getLastLogTimeStamp()
    {
        return lastLogTimeStamp;
    }

    public String getExpireDays()
    {
        return expireDays;
    }

    public String getCreatedAt()
    {
        return createdAt;
    }

    public String getUpdatedAt()
    {
        return updatedAt;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        TDTable other = (TDTable) obj;
        return Objects.equal(this.name, other.name) &&
                Objects.equal(this.type, other.type) &&
                Objects.equal(this.columns, other.columns);
    }

    @Override
    public int hashCode()
    {
        return Objects.hashCode(name, type, columns);
    }

    @Override
    public String toString()
    {
        return String.format("%s(%s)", name, Joiner.on(", ").join(columns));
    }
} public class TDTableDistribution
{
    private final long userTableId;
    private final long bucketCount;
    private final String partitionFunction;
    private final List<TDColumn> columns;

    @JsonCreator
    public TDTableDistribution(
            @JsonProperty("user_table_id") long userTableId,
            @JsonProperty("bucket_count") long bucketCount,
            @JsonProperty("partition_function") String partitionFunction,
            @JsonProperty("columns") List<TDColumn> columns)
    {
        this.userTableId = userTableId;
        this.bucketCount = bucketCount;
        this.partitionFunction = partitionFunction;
        this.columns = columns;
    }

    @JsonProperty
    public long getUserTableId()
    {
        return userTableId;
    }

    /**
     * The maximum number of buckets of UDP table.
     */
    @JsonProperty
    public long getBucketCount()
    {
        return bucketCount;
    }

    /**
     * Hash function to calculate the partitioning key.
     */
    @JsonProperty
    public String getPartitionFunction()
    {
        return partitionFunction;
    }

    /**
     * Columns used for partitioning key in UDP table
     */
    @JsonProperty
    public List<TDColumn> getColumns()
    {
        return columns;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        TDTableDistribution other = (TDTableDistribution) obj;
        return Objects.equal(this.userTableId, other.userTableId) &&
                Objects.equal(this.bucketCount, other.bucketCount) &&
                Objects.equal(this.partitionFunction, other.partitionFunction) &&
                Objects.equal(this.columns, other.columns);
    }

    @Override
    public int hashCode()
    {
        return Objects.hashCode(userTableId, bucketCount, partitionFunction);
    }

    @Override
    public String toString()
    {
        return String.format("userTableId: %s, bucketCount: %s, partitionFunction: %s, columns: %s",
                userTableId, bucketCount, partitionFunction, columns);
    }
} public class TDTableList
{
    private String name;
    private List<TDTable> tables;

    @JsonCreator
    public TDTableList(
            @JsonProperty("name") String name,
            @JsonProperty("tables") List<TDTable> tables)
    {
        this.name = name;
        this.tables = tables;
    }

    @JsonProperty
    public String getName()
    {
        return name;
    }

    @JsonProperty
    public List<TDTable> getTables()
    {
        return tables;
    }
} public enum TDTableType
{
    LOG("log"),
    ITEM("item");
    private String name;

    private TDTableType(String name)
    {
        this.name = name;
    }

    @JsonCreator
    public static TDTableType fromName(String name)
    {
        if ("log".equals(name)) {
            return LOG;
        }
        else if ("item".equals(name)) {
            return ITEM;
        }
        throw new RuntimeJsonMappingException("Unexpected string tuple to deserialize TDTableType");
    }

    public String getTypeName()
    {
        return name;
    }

    @JsonValue
    @Override
    public String toString()
    {
        return name;
    }
} public class TDUpdateTableResult
{
    private final String database;
    private final String table;
    private final TDTableType type;

    @JsonCreator
    public TDUpdateTableResult(
            @JsonProperty("database") String database,
            @JsonProperty("table") String table,
            @JsonProperty("type") TDTableType type)
    {
        this.database = database;
        this.table = table;
        this.type = type;
    }

    public String getDatabase()
    {
        return database;
    }

    public String getTable()
    {
        return table;
    }

    public TDTableType getType()
    {
        return type;
    }
} @Value.Immutable
@Value.Style(visibility = Value.Style.ImplementationVisibility.PACKAGE)
@JsonSerialize(as = ImmutableTDUser.class)
@JsonDeserialize(as = ImmutableTDUser.class)
public abstract class TDUser
{
    @JsonSerialize(using = StringToNumberSerializer.class) @JsonProperty("id") public abstract String getId();
    @JsonProperty("name") public abstract String getName();
    @JsonProperty("first_name") public abstract Optional<String> getFirstName();
    @JsonProperty("last_name") public abstract Optional<String> getLastName();
    @JsonProperty("email") public abstract String getEmail();
    @JsonProperty("phone") public abstract Optional<String> getPhone();
    @JsonProperty("gravatar_url") public abstract String getGravatarUrl();
    @JsonProperty("administrator") public abstract boolean isAdministrator();
    @JsonProperty("created_at") public abstract String getCreatedAt();
    @JsonProperty("updated_at") public abstract String getUpdatedAt();
    @JsonProperty("account_owner") public abstract boolean isAccountOwner();

    public interface Builder
    {
        Builder id(String id);
        Builder name(String name);
        Builder firstName(String firstName);
        Builder lastName(String lastName);
        Builder email(String email);
        Builder phone(String phone);
        Builder gravatarUrl(String gravatarUrl);
        Builder isAdministrator(boolean administrator);
        Builder createdAt(String createdAt);
        Builder updatedAt(String updatedAt);
        Builder isAccountOwner(boolean accountOwner);
        TDUser build();
    }

    public static Builder builder()
    {
        return ImmutableTDUser.builder();
    }
} public class TDUserList
{
    private final List<TDUser> users;

    @JsonCreator
    TDUserList(@JsonProperty("users") List<TDUser> users)
    {
        this.users = checkNotNull(users, "users");
    }

    @JsonProperty("users")
    public List<TDUser> getUsers()
    {
        return users;
    }

    static TDUserList of(List<TDUser> users)
    {
        return new TDUserList(users);
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        TDUserList that = (TDUserList) o;

        return users != null ? users.equals(that.users) : that.users == null;
    }

    @Override
    public int hashCode()
    {
        return users != null ? users.hashCode() : 0;
    }

    @Override
    public String toString()
    {
        return "TDUserList{" +
                "users=" + users +
                '}';
    }
} public class TDScheduleRunResult
{
    public static class Job
    {
        private final String jobId;
        private final TDJob.Type jobType;
        private final String scheduledAt;

        public Job(
                @JsonProperty("job_id") String jobId,
                @JsonProperty("job_type") TDJob.Type jobType,
                @JsonProperty("scheduled_at") String scheduledAt)
        {
            this.jobId = jobId;
            this.jobType = jobType;
            this.scheduledAt = scheduledAt;
        }

        @JsonProperty("job_id")
        public String getJobId()
        {
            return jobId;
        }

        @JsonProperty("type")
        public TDJob.Type getType()
        {
            return jobType;
        }

        @JsonProperty("scheduled_at")
        public String getScheduledAt()
        {
            return scheduledAt;
        }

        @Override
        public String toString()
        {
            return "Job{" +
                    "jobId='" + jobId + '\'' +
                    '}';
        }
    }

    private final List<Job> jobs;

    @JsonCreator
    public TDScheduleRunResult(@JsonProperty("jobs") List<Job> jobs)
    {
        this.jobs = ImmutableList.copyOf(jobs);
    }

    @JsonProperty("jobs")
    public List<Job> getJobs()
    {
        return jobs;
    }

    @Override
    public String toString()
    {
        return "TDScheduleRunResult{" +
                "jobs=" + jobs +
                '}';
    }
}
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.treasuredata.client.model.ObjectMappers;
import com.treasuredata.client.model.TDApiKey;
import com.treasuredata.client.model.TDAuthenticationResult;
import com.treasuredata.client.model.TDBulkImportParts;
import com.treasuredata.client.model.TDBulkImportSession;
import com.treasuredata.client.model.TDBulkLoadSessionStartRequest;
import com.treasuredata.client.model.TDBulkLoadSessionStartResult;
import com.treasuredata.client.model.TDColumn;
import com.treasuredata.client.model.TDConnectionLookupResult;
import com.treasuredata.client.model.TDDatabase;
import com.treasuredata.client.model.TDExportJobRequest;
import com.treasuredata.client.model.TDExportResultJobRequest;
import com.treasuredata.client.model.TDImportResult;
import com.treasuredata.client.model.TDJob;
import com.treasuredata.client.model.TDJobList;
import com.treasuredata.client.model.TDJobRequest;
import com.treasuredata.client.model.TDJobSubmitResult;
import com.treasuredata.client.model.TDJobSummary;
import com.treasuredata.client.model.TDPartialDeleteJob;
import com.treasuredata.client.model.TDResultFormat;
import com.treasuredata.client.model.TDSaveQueryRequest;
import com.treasuredata.client.model.TDSavedQuery;
import com.treasuredata.client.model.TDSavedQueryHistory;
import com.treasuredata.client.model.TDSavedQueryStartRequest;
import com.treasuredata.client.model.TDSavedQueryStartRequestV4;
import com.treasuredata.client.model.TDSavedQueryStartResultV4;
import com.treasuredata.client.model.TDSavedQueryUpdateRequest;
import com.treasuredata.client.model.TDTable;
import com.treasuredata.client.model.TDTableDistribution;
import com.treasuredata.client.model.TDTableList;
import com.treasuredata.client.model.TDTableType;
import com.treasuredata.client.model.TDUpdateTableResult;
import com.treasuredata.client.model.TDUser;
import com.treasuredata.client.model.TDUserList;
import com.treasuredata.client.model.impl.TDScheduleRunResult;
import org.json.simple.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.UrlEscapers.urlPathSegmentEscaper;

    @Override
    public TDSavedQuery deleteSavedQuery(String name)
    {
        TDSavedQuery result = doPost(
                buildUrl("/v3/schedule/delete", name),
                TDSavedQuery.class);
        return result;
    }

    @Override
    public String submitExportJob(TDExportJobRequest jobRequest)
            throws TDClientException
    {
        Map<String, String> queryParam = new HashMap<>();
        queryParam.put("from", Long.toString(jobRequest.getFrom().getTime() / 1000));
        queryParam.put("to", Long.toString(jobRequest.getTo().getTime() / 1000));
        queryParam.put("file_prefix", jobRequest.getFilePrefix());
        queryParam.put("file_format", jobRequest.getFileFormat().toString());
        queryParam.put("storage_type", "s3");
        queryParam.put("bucket", jobRequest.getBucketName());
        queryParam.put("access_key_id", jobRequest.getAccessKeyId());
        queryParam.put("secret_access_key", jobRequest.getSecretAccessKey());

        if (jobRequest.getPoolName().isPresent()) {
            queryParam.put("pool_name", jobRequest.getPoolName().get());
        }

        Optional<String> domainKey = jobRequest.getDomainKey();
        if (domainKey.isPresent()) {
            queryParam.put("domain_key", domainKey.get());
        }

        if (logger.isDebugEnabled()) {
            logger.debug("submit job: " + jobRequest);
        }

        TDJobSubmitResult result =
                doPost(
                        buildUrl("/v3/export/run", jobRequest.getDatabase(), jobRequest.getTable()),
                        queryParam,
                        TDJobSubmitResult.class);
        return result.getJobId();
    }

    @Override
    public TDBulkLoadSessionStartResult startBulkLoadSession(String name)
    {
        TDBulkLoadSessionStartRequest request = TDBulkLoadSessionStartRequest.builder()
                .build();
        return startBulkLoadSession(name, request);
    }

    @Override
    public TDBulkLoadSessionStartResult startBulkLoadSession(String name, long scheduledTime)
    {
        TDBulkLoadSessionStartRequest request = TDBulkLoadSessionStartRequest.builder()
                .setScheduledTime(String.valueOf(scheduledTime))
                .build();
        return startBulkLoadSession(name, request);
    }

    @Override
    public TDBulkLoadSessionStartResult startBulkLoadSession(String name, TDBulkLoadSessionStartRequest request)
    {
        Map<String, String> queryParams = ImmutableMap.of();
        String payload = null;
        try {
            payload = ObjectMappers.compactMapper().writeValueAsString(request);
        }
        catch (JsonProcessingException e) {
            throw Throwables.propagate(e);
        }
        return doPost(buildUrl("/v3/bulk_loads", name, "jobs"),
                queryParams, Optional.of(payload),
                TDBulkLoadSessionStartResult.class);
    }

    @Override
    public long lookupConnection(String name)
    {
        return doGet(buildUrl("/v3/connections/lookup?name=" + urlPathSegmentEscaper().escape(name)), TDConnectionLookupResult.class).getId();
    }

    @Override
    public String submitResultExportJob(TDExportResultJobRequest jobRequest)
    {
        Map<String, String> queryParam = new HashMap<>();
        if (!jobRequest.getResultConnectionId().isEmpty() && !jobRequest.getResultConnectionSettings().isEmpty()) {
            queryParam.put("result_connection_id", jobRequest.getResultConnectionId());
            queryParam.put("result_connection_settings", jobRequest.getResultConnectionSettings());
        }
        else if (!jobRequest.getResultOutput().isEmpty()) {
            queryParam.put("result", jobRequest.getResultOutput());
        }
        else {
            throw new IllegalStateException("Either resultOutput or a pair of resultConnectionId and resultConnectionSettings is required");
        }

        TDJobSubmitResult result = doPost(
                buildUrl("/v3/job/result_export", jobRequest.getJobId()),
                queryParam,
                TDJobSubmitResult.class);

        return result.getJobId();
    }

    @Override
    public Optional<TDTableDistribution> tableDistribution(String databaseName, String tableName)
    {
        try {
            TDTableDistribution distribution = doGet(buildUrl(String.format("/v3/table/distribution/%s/%s", databaseName, tableName)), TDTableDistribution.class);
            return Optional.of(distribution);
        }
        catch (TDClientHttpNotFoundException e) {
            return Optional.absent();
        }
    }

    @Override
    public TDImportResult importFile(String databaseName, String tableName, File file)