All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.g11n.pipeline.client.impl.ServiceClientImpl Maven / Gradle / Ivy

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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import com.google.common.net.UrlEscapers;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.ibm.g11n.pipeline.client.BundleData;
import com.ibm.g11n.pipeline.client.BundleDataChangeSet;
import com.ibm.g11n.pipeline.client.BundleMetrics;
import com.ibm.g11n.pipeline.client.DocumentData;
import com.ibm.g11n.pipeline.client.DocumentDataChangeSet;
import com.ibm.g11n.pipeline.client.DocumentMetrics;
import com.ibm.g11n.pipeline.client.DocumentTranslationRequestData;
import com.ibm.g11n.pipeline.client.DocumentTranslationRequestDataChangeSet;
import com.ibm.g11n.pipeline.client.DocumentType;
import com.ibm.g11n.pipeline.client.LanguageMetrics;
import com.ibm.g11n.pipeline.client.MTServiceBindingData;
import com.ibm.g11n.pipeline.client.NewBundleData;
import com.ibm.g11n.pipeline.client.NewDocumentData;
import com.ibm.g11n.pipeline.client.NewDocumentTranslationRequestData;
import com.ibm.g11n.pipeline.client.NewResourceEntryData;
import com.ibm.g11n.pipeline.client.NewTranslationConfigData;
import com.ibm.g11n.pipeline.client.NewTranslationRequestData;
import com.ibm.g11n.pipeline.client.NewUserData;
import com.ibm.g11n.pipeline.client.ResourceEntryData;
import com.ibm.g11n.pipeline.client.ResourceEntryDataChangeSet;
import com.ibm.g11n.pipeline.client.ReviewStatusMetrics;
import com.ibm.g11n.pipeline.client.SegmentData;
import com.ibm.g11n.pipeline.client.ServiceAccount;
import com.ibm.g11n.pipeline.client.ServiceClient;
import com.ibm.g11n.pipeline.client.ServiceException;
import com.ibm.g11n.pipeline.client.ServiceInfo;
import com.ibm.g11n.pipeline.client.ServiceInstanceInfo;
import com.ibm.g11n.pipeline.client.TranslationConfigData;
import com.ibm.g11n.pipeline.client.TranslationRequestData;
import com.ibm.g11n.pipeline.client.TranslationRequestDataChangeSet;
import com.ibm.g11n.pipeline.client.TranslationRequestStatus;
import com.ibm.g11n.pipeline.client.TranslationStatus;
import com.ibm.g11n.pipeline.client.UserData;
import com.ibm.g11n.pipeline.client.UserDataChangeSet;
import com.ibm.g11n.pipeline.client.impl.BundleDataImpl.RestBundle;
import com.ibm.g11n.pipeline.client.impl.DocumentDataImpl.RestDocument;
import com.ibm.g11n.pipeline.client.impl.DocumentTranslationRequestDataImpl.RestDocumentTranslationRequest;
import com.ibm.g11n.pipeline.client.impl.DocumentTranslationRequestDataImpl.RestInputDocumentTranslationRequestData;
import com.ibm.g11n.pipeline.client.impl.MTServiceBindingDataImpl.RestMTServiceBinding;
import com.ibm.g11n.pipeline.client.impl.ResourceEntryDataImpl.RestResourceEntry;
import com.ibm.g11n.pipeline.client.impl.SegmentDataImpl.RestSegmentData;
import com.ibm.g11n.pipeline.client.impl.ServiceInfoImpl.ExternalServiceInfoImpl.RestExternalServiceInfo;
import com.ibm.g11n.pipeline.client.impl.ServiceInstanceInfoImpl.RestServiceInstanceInfo;
import com.ibm.g11n.pipeline.client.impl.ServiceResponse.Status;
import com.ibm.g11n.pipeline.client.impl.TranslationConfigDataImpl.RestTranslationConfigData;
import com.ibm.g11n.pipeline.client.impl.TranslationRequestDataImpl.RestInputTranslationRequestData;
import com.ibm.g11n.pipeline.client.impl.TranslationRequestDataImpl.RestTranslationRequest;
import com.ibm.g11n.pipeline.client.impl.UserDataImpl.RestUser;
import com.ibm.g11n.pipeline.iam.TokenManagerException;

/**
 * ServiceClient implementation by GSON and JDK's HttpURLConnection.
 * 
 * @author Yoshito Umaoka
 */
public class ServiceClientImpl extends ServiceClient {

    public ServiceClientImpl(ServiceAccount account) {
        super(account);
    }

    //
    // Service API
    //

    private static class GetServiceInfoResponse extends ServiceResponse {
        Map> supportedTranslation;
        Collection externalServices;
    }

    @Override
    public ServiceInfo getServiceInfo() throws ServiceException {
        GetServiceInfoResponse resp = invokeApiJson(
                "GET",
                "$service/v2/info",
                null,
                GetServiceInfoResponse.class,
                true);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new ServiceInfoImpl(resp.supportedTranslation, resp.externalServices);
    }

    //
    // Instance API
    //

    private static class GetServiceInstanceInfoResponse extends ServiceResponse {
        RestServiceInstanceInfo instance;
    }

    @Override
    public ServiceInstanceInfo getServiceInstanceInfo() throws ServiceException {
        GetServiceInstanceInfoResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/instance/info",
                null,
                GetServiceInstanceInfoResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new ServiceInstanceInfoImpl(resp.instance);
    }

    //
    // Bundle API
    //

    private static class GetBundleListResponse extends ServiceResponse {
        Set bundleIds;
    }

    @Override
    public Set getBundleIds() throws ServiceException {
        GetBundleListResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles",
                null,
                GetBundleListResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return resp.bundleIds;
    }


    @Override
    public void createBundle(String bundleId, NewBundleData newBundleData)
            throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }
        if (newBundleData == null) {
            throw new IllegalArgumentException("newBundleData must be specified.");
        }

        Gson gson = createGson(NewBundleData.class.getName());
        String jsonBody = gson.toJson(newBundleData, NewBundleData.class);
        ServiceResponse resp = invokeApiJson(
                "PUT",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId),
                jsonBody,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }

    private static class GetBundleInfoResponse extends ServiceResponse {
        RestBundle bundle;
    }

    @Override
    public BundleData getBundleInfo(String bundleId) throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }

        GetBundleInfoResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId),
                null,
                GetBundleInfoResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new BundleDataImpl(resp.bundle);
    }

    private static class GetBundleMetricsResponse extends ServiceResponse {
        private Map> translationStatusMetricsByLanguage;
        private Map reviewStatusMetricsByLanguage;
        private Map> partnerStatusMetricsByLanguage;
    }

    @Override
    public BundleMetrics getBundleMetrics(String bundleId) throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }

        GetBundleMetricsResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId)
                    + "?fields=translationStatusMetricsByLanguage,reviewStatusMetricsByLanguage,partnerStatusMetricsByLanguage",
                null,
                GetBundleMetricsResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new BundleMetricsImpl(
                resp.translationStatusMetricsByLanguage,
                resp.reviewStatusMetricsByLanguage,
                resp.partnerStatusMetricsByLanguage);
    }

    @Override
    public void updateBundle(String bundleId, BundleDataChangeSet changeSet)
            throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }
        if (changeSet == null) {
            throw new IllegalArgumentException("changeSet must be specified.");
        }

        Gson gson = createGson(BundleDataChangeSet.class.getName());
        String jsonBody = gson.toJson(changeSet, BundleDataChangeSet.class);
        ServiceResponse resp = invokeApiJson(
                "POST",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId),
                jsonBody,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }

    @Override
    public void deleteBundle(String bundleId) throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }

        GetBundleInfoResponse resp = invokeApiJson(
                "DELETE",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId),
                null,
                GetBundleInfoResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }

    private static class GetResourceStringsResponse extends ServiceResponse {
        Map resourceStrings;
    }

    @Override
    public Map getResourceStrings(String bundleId,
            String language, boolean fallback) throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }
        if (language == null || language.isEmpty()) {
            throw new IllegalArgumentException("language must be specified.");
        }

        StringBuilder endpoint = new StringBuilder();
        endpoint
            .append(escapePathSegment(account.getInstanceId()))
            .append("/v2/bundles/")
            .append(escapePathSegment(bundleId))
            .append("/")
            .append(language);
        if (fallback) {
            endpoint.append("?fallback=true");
        }

        GetResourceStringsResponse resp = invokeApiJson(
                "GET",
                endpoint.toString(),
                null,
                GetResourceStringsResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return resp.resourceStrings;
    }

    private static class GetResourceEntriesResponse extends ServiceResponse {
        Map resourceEntries;
    }

    @Override
    public Map getResourceEntries(String bundleId,
            String language) throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }
        if (language == null || language.isEmpty()) {
            throw new IllegalArgumentException("language must be specified.");
        }

        GetResourceEntriesResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId) + "/" + language
                    + "?fields=resourceEntries",
                null,
                GetResourceEntriesResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        Map resultEntries = new TreeMap();
        if (resp.resourceEntries != null && !resp.resourceEntries.isEmpty()) {
            for (Entry entry : resp.resourceEntries.entrySet()) {
                resultEntries.put(entry.getKey(),
                        new ResourceEntryDataImpl(entry.getValue()));
            }
        }
        return resultEntries;
    }

    private static class GetLanguageMetricsResponse extends ServiceResponse {
        private EnumMap translationStatusMetrics;
        private ReviewStatusMetrics reviewStatusMetrics;
        private Map partnerStatusMetrics;
    }

    @Override
    public LanguageMetrics getLanguageMetrics(String bundleId, String language)
            throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }
        if (language == null || language.isEmpty()) {
            throw new IllegalArgumentException("language must be specified.");
        }

        GetLanguageMetricsResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId) + "/" + language
                    + "?fields=translationStatusMetrics,reviewStatusMetrics,partnerStatusMetrics",
                null,
                GetLanguageMetricsResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new LanguageMetricsImpl(resp.translationStatusMetrics,
                resp.reviewStatusMetrics, resp.partnerStatusMetrics);
    }


    @Override
    public void uploadResourceStrings(String bundleId, String language,
            Map strings) throws ServiceException {
        if (strings == null || strings.isEmpty()) {
            throw new IllegalArgumentException("strings must be specified.");
        }
        Map newResourceEntries = new HashMap<>(strings.size());
        for (Entry res : strings.entrySet()) {
            String key = res.getKey();
            String value = res.getValue();
            if (value == null) {
                newResourceEntries.put(key, null);
            } else {
                NewResourceEntryData newEntry = new NewResourceEntryData(value);
                newResourceEntries.put(key, newEntry);
            }
        }
        uploadResourceEntries(bundleId, language, newResourceEntries);
    }

    @Override
    public void uploadResourceEntries(String bundleId, String language,
            Map newResourceEntries)
            throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }
        if (language == null || language.isEmpty()) {
            throw new IllegalArgumentException("language must be specified.");
        }
        if (newResourceEntries == null || newResourceEntries.isEmpty()) {
            throw new IllegalArgumentException("newResourceEntries must be specified.");
        }

        Gson gson = createGson(Map.class.getName());
        String jsonBody = gson.toJson(newResourceEntries, Map.class);
        ServiceResponse resp = invokeApiJson(
                "PUT",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId) + "/" + language,
                jsonBody,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }

    @Override
    public void updateResourceStrings(String bundleId, String language,
            Map strings, boolean resync)
                    throws ServiceException {
        if ((strings == null || strings.isEmpty()) && !resync) {
            throw new IllegalArgumentException("strings must be specified when resync is false.");
        }

        Map resourceEntries = null;
        if (strings != null) {
            resourceEntries = new HashMap<>(strings.size());
            for (Entry stringRes : strings.entrySet()) {
                String key = stringRes.getKey();
                String value = stringRes.getValue();
                if (value == null) {
                    resourceEntries.put(key, null);
                } else {
                    ResourceEntryDataChangeSet entry = new ResourceEntryDataChangeSet();
                    entry.setValue(value);
                    resourceEntries.put(key, entry);
                }
            }
        }
        updateResourceEntries(bundleId, language, resourceEntries, resync);
    }

    @Override
    public void updateResourceEntries(String bundleId, String language,
            Map resourceEntries, boolean resync)
            throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }
        if (language == null || language.isEmpty()) {
            throw new IllegalArgumentException("language must be specified.");
        }

        String jsonBody = null;
        if (resourceEntries == null || resourceEntries.isEmpty()) {
            jsonBody = "{}";
        } else {
            Gson gson = createGson(Map.class.getName());
            jsonBody = gson.toJson(resourceEntries, Map.class);
        }

        ServiceResponse resp = invokeApiJson(
                "POST",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId) + "/" + language,
                jsonBody,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }

    private static class GetResourceEntryResponse extends ServiceResponse {
        RestResourceEntry resourceEntry;
    }

    @Override
    public ResourceEntryData getResourceEntry(String bundleId, String language,
            String resKey) throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }
        if (language == null || language.isEmpty()) {
            throw new IllegalArgumentException("language must be specified.");
        }
        if (resKey == null || resKey.isEmpty()) {
            throw new IllegalArgumentException("resKey must be specified.");
        }

        GetResourceEntryResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId) + "/" + language
                    + "/" + escapePathSegment(resKey),
                null,
                GetResourceEntryResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new ResourceEntryDataImpl(resp.resourceEntry);
    }

    @Override
    public void updateResourceEntry(String bundleId, String language,
            String resKey, ResourceEntryDataChangeSet changeSet)
                    throws ServiceException {
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("bundleId must be specified.");
        }
        if (language == null || language.isEmpty()) {
            throw new IllegalArgumentException("language must be specified.");
        }
        if (resKey == null || resKey.isEmpty()) {
            throw new IllegalArgumentException("resKey must be specified.");
        }
        if (changeSet == null) {
            throw new IllegalArgumentException("changeSet must be specified.");
        }

        Gson gson = createGson(ResourceEntryDataChangeSet.class.getName());
        String jsonBody = gson.toJson(changeSet, ResourceEntryDataChangeSet.class);
        ServiceResponse resp = invokeApiJson(
                "POST",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId) + "/" + language
                    + "/" + escapePathSegment(resKey),
                jsonBody,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }

    //
    // Document API
    //

    private static class GetDocumentListResponse extends ServiceResponse {
        Set documentDataSet;
    }
    
    @Override
    public Set getDocumentIds(DocumentType type) throws ServiceException {
        GetDocumentListResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/documents/" + type.toString().toLowerCase(),
                null,
                GetDocumentListResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        Set result = new TreeSet<>();
        if (resp.documentDataSet != null) {
            for (RestDocument doc : resp.documentDataSet) {
                result.add(doc.getDocumentId());
            }
        }
        return result;
    }

    @Override
    public void createDocument(DocumentType type, String documentId, NewDocumentData newDocumentData)
            throws ServiceException {
        if (newDocumentData == null) {
            throw new IllegalArgumentException("newDocumentData must be specified.");
        }

        Gson gson = createGson(NewBundleData.class.getName());
        String jsonBody = gson.toJson(newDocumentData, NewDocumentData.class);
        ServiceResponse resp = invokeApiJson(
                "PUT",
                escapePathSegment(account.getInstanceId()) + "/v2/documents/"
                    + type.toString().toLowerCase() + "/"
                    + escapePathSegment(documentId),
                jsonBody,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }
    
    private static class GetDocumentInfoResponse extends ServiceResponse {
        RestDocument documentData;
    }

    @Override
    public DocumentData getDocumentInfo(DocumentType type, String documentId) throws ServiceException {
        if (Strings.isNullOrEmpty(documentId)) {
            throw new IllegalArgumentException("documentId must be specified.");
        }

        GetDocumentInfoResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/documents/"
                    + type.toString().toLowerCase() + "/"
                    + escapePathSegment(documentId),
                null,
                GetDocumentInfoResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new DocumentDataImpl(resp.documentData);
    }

    private static class GetDocumentMetricsResponse extends ServiceResponse {
        private Map> translationStatusMetricsByLanguage;
        private Map reviewStatusMetricsByLanguage;
    }

    @Override
    public DocumentMetrics getDocumentMetrics(DocumentType type, String documentId) throws ServiceException {
        if (Strings.isNullOrEmpty(documentId)) {
            throw new IllegalArgumentException("documentId must be specified.");
        }

        GetDocumentMetricsResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/documents/"
                    + type.toString().toLowerCase() + "/"
                    + escapePathSegment(documentId),
                null,
                GetDocumentMetricsResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new DocumentMetricsImpl(
                resp.translationStatusMetricsByLanguage,
                resp.reviewStatusMetricsByLanguage);
    }

    @Override
    public void updateDocument(DocumentType type, String documentId, DocumentDataChangeSet changeSet)
            throws ServiceException {
        if (Strings.isNullOrEmpty(documentId)) {
            throw new IllegalArgumentException("documentId must be specified.");
        }
        if (changeSet == null) {
            throw new IllegalArgumentException("changeSet must be specified.");
        }

        Gson gson = createGson(DocumentDataChangeSet.class.getName());
        String jsonBody = gson.toJson(changeSet, DocumentDataChangeSet.class);
        ServiceResponse resp = invokeApiJson(
                "POST",
                escapePathSegment(account.getInstanceId()) + "/v2/documents/"
                    + type.toString().toLowerCase() + "/"
                    + escapePathSegment(documentId),
                jsonBody,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }
    
    @Override
    public void deleteDocument(DocumentType type, String documentId) throws ServiceException {

        GetDocumentInfoResponse resp = invokeApiJson(
                "DELETE",
                escapePathSegment(account.getInstanceId()) + "/v2/documents/"
                    + type.toString().toLowerCase() + "/"
                    + escapePathSegment(documentId),
                null,
                GetDocumentInfoResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }

    @Override
    public void updateDocumentContent(DocumentType type, String documentId, String language,
            File file)
            throws ServiceException {
        if (Strings.isNullOrEmpty(documentId)) {
            throw new IllegalArgumentException("documentId must be specified.");
        }
        if (Strings.isNullOrEmpty(language)) {
            throw new IllegalArgumentException("language must be specified.");
        }
        if (file == null || !file.isFile()) {
            throw new IllegalArgumentException("file must be a regular file.");
        }

        FileInputStream fis;
        try {
            fis = new FileInputStream(file);
        } catch (IOException e) {
            throw new RuntimeException("Failed to read the document content from "
                    + file.getName() + ": " + e.getMessage(), e);
        }

        ServiceResponse resp = invokeApiInputStream(
                "PUT",
                escapePathSegment(account.getInstanceId()) + "/v2/documents/"
                        + type.toString().toLowerCase() + "/"
                        + documentId + "/"
                        + language,
                type.getMediaType(),
                fis,
                ServiceResponse.class,
                false);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

    }
    
    @Override
    public byte[] getDocumentContent(DocumentType type, String documentId, String language)
            throws ServiceException {
        if (Strings.isNullOrEmpty(documentId)) {
            throw new IllegalArgumentException("documentId must be specified.");
        }
        if (Strings.isNullOrEmpty(language)) {
            throw new IllegalArgumentException("language must be specified.");
        }

        ApiResponse resp;
        String method = "GET";
        String apiPath = escapePathSegment(account.getInstanceId()) + "/v2/documents/"
                        + type.toString().toLowerCase() + "/"
                        + documentId + "/"
                        + language;
                
        try {
            resp = invokeApi(method,apiPath,null,null,false);
        } catch (Exception e) {
            String errMsg = "Error while processing API request GET " + apiPath;
            throw new ServiceException(errMsg, e);
        }
        
        if (resp.status >= 300) {
            String bodyStr = resp.body != null ? new String(resp.body, StandardCharsets.UTF_8) : null;
            throw new ServiceException("Received HTTP status: " + resp.status + " from " + method
                    + " " + apiPath + ", body: " + bodyStr);
        }
        return resp.body;
    }

    @Override
    public void writeDocumentContent(DocumentType type, String documentId, String language, OutputStream os)
            throws IllegalArgumentException, ServiceException, IOException {
        
        os.write(getDocumentContent(type, documentId, language));
    }

    //
    // User API
    //

    private static class GetUsersResponse extends ServiceResponse {
        Map users;
    }

    @Override
    public Map getUsers() throws ServiceException {
        GetUsersResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/users",
                null,
                GetUsersResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        Map resultUsers = new TreeMap();
        for (Entry userEntry : resp.users.entrySet()) {
            String id = userEntry.getKey();
            RestUser user = userEntry.getValue();
            resultUsers.put(id, new UserDataImpl(user));
        }

        return resultUsers;
    }

    private static class UserResponse extends ServiceResponse {
        @SuppressWarnings("unused")
        String id;
        RestUser user;
    }

    @Override
    public UserData createUser(NewUserData newUserData)
            throws ServiceException {
        if (newUserData == null) {
            throw new IllegalArgumentException("newUserData must be specified.");
        }

        Gson gson = createGson(NewUserData.class.getName());
        String jsonBody = gson.toJson(newUserData, NewUserData.class);
        UserResponse resp = invokeApiJson(
                "POST",
                escapePathSegment(account.getInstanceId()) + "/v2/users/new",
                jsonBody,
                UserResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new UserDataImpl(resp.user);
    }

    @Override
    public UserData getUser(String userId) throws ServiceException {
        if (userId == null || userId.isEmpty()) {
            throw new IllegalArgumentException("userId must be specified.");
        }

        UserResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/users/"
                    + escapePathSegment(userId),
                null,
                UserResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new UserDataImpl(resp.user);
    }

    @Override
    public UserData updateUser(String userId, UserDataChangeSet changeSet,
            boolean resetPassword) throws ServiceException {
        if (userId == null || userId.isEmpty()) {
            throw new IllegalArgumentException("userId must be specified.");
        }
        if (changeSet == null && !resetPassword) {
            throw new IllegalArgumentException("changeSet must be specified when resetPassword is false");
        }

        StringBuilder endpoint = new StringBuilder();
        endpoint
            .append(escapePathSegment(account.getInstanceId()))
            .append("/v2/users/")
            .append(escapePathSegment(userId));
        if (resetPassword) {
            endpoint.append("?resetPassword=true");
        }

        String jsonBody = null;
        if (changeSet == null) {
            jsonBody = "{}";
        } else {
            Gson gson = createGson(UserDataChangeSet.class.getName());
            jsonBody = gson.toJson(changeSet, UserDataChangeSet.class);
        }

        UserResponse resp = invokeApiJson(
                "POST",
                endpoint.toString(),
                jsonBody,
                UserResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new UserDataImpl(resp.user);
    }

    @Override
    public void deleteUser(String userId) throws ServiceException {
        if (userId == null || userId.isEmpty()) {
            throw new IllegalArgumentException("userId must be specified.");
        }

        ServiceResponse resp = invokeApiJson(
                "DELETE",
                escapePathSegment(account.getInstanceId()) + "/v2/users/"
                    + escapePathSegment(userId),
                null,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }


    //
    // Config API
    //

    private static class MTBindingsResponse extends ServiceResponse {
        Map mtServiceBindings;
    }

    @Override
    public Map getAllMTServiceBindings() throws ServiceException {
        MTBindingsResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/config/mt",
                null,
                MTBindingsResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        Map resultBindings = new TreeMap<>();
        for (Entry entry : resp.mtServiceBindings.entrySet()) {
            resultBindings.put(entry.getKey(), new MTServiceBindingDataImpl(entry.getValue()));
        }
        return resultBindings;
    }

    private static class AvailableMTLanguagesResponse extends ServiceResponse {
        Map>> availableLanguages;
    }

    @Override
    public Map>> getAvailableMTLanguages()
            throws ServiceException {
        AvailableMTLanguagesResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/config/mt",
                null,
                AvailableMTLanguagesResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return resp.availableLanguages;
    }

    private static class GetMTServiceBindingResponse extends ServiceResponse {
        RestMTServiceBinding mtServiceBinding;
    }

    @Override
    public MTServiceBindingData getMTServiceBinding(String mtServiceInstanceId)
            throws ServiceException {
        GetMTServiceBindingResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/config/mt/"
                    + escapePathSegment(mtServiceInstanceId),
                null,
                GetMTServiceBindingResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new MTServiceBindingDataImpl(resp.mtServiceBinding);
    }

    private static class TranslationConfigsResponse extends ServiceResponse {
        Map> translationConfigs;
    }

    @Override
    public Map> getAllTranslationConfigs()
            throws ServiceException {
        TranslationConfigsResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/config/trans",
                null,
                TranslationConfigsResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return resp.translationConfigs;
    }

    private static class ConfiguredMTLanguagesResponse extends ServiceResponse {
        Map> mtLanguages;
    }

    @Override
    public Map> getConfiguredMTLanguages()
            throws ServiceException {
        ConfiguredMTLanguagesResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/config/trans",
                null,
                ConfiguredMTLanguagesResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return resp.mtLanguages;
    }

    @Override
    public void putTranslationConfig(String sourceLanguage, String targetLanguage,
            NewTranslationConfigData configData) throws ServiceException {
        if (configData == null) {
            throw new IllegalArgumentException("configData must be specified");
        }

        Gson gson = createGson(NewTranslationConfigData.class.getName());
        String jsonBody = gson.toJson(configData, NewTranslationConfigData.class);

        ServiceResponse resp = invokeApiJson(
                "PUT",
                escapePathSegment(account.getInstanceId()) + "/v2/config/trans/"
                    + sourceLanguage + "/" + targetLanguage,
                jsonBody,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }

    private static class TranslationConfigResponse extends ServiceResponse {
        RestTranslationConfigData config;
    }

    @Override
    public TranslationConfigData getTranslationConfig(String sourceLanguage,
            String targetLanguage) throws ServiceException {
        TranslationConfigResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/config/trans/"
                    + sourceLanguage + "/" + targetLanguage,
                null,
                TranslationConfigResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new TranslationConfigDataImpl(resp.config);
    }


    @Override
    public void deleteTranslationConfig(String sourceLanguage,
            String targetLanguage) throws ServiceException {
        ServiceResponse resp = invokeApiJson(
                "DELETE",
                escapePathSegment(account.getInstanceId()) + "/v2/config/trans/"
                    + sourceLanguage + "/" + targetLanguage,
                null,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }


    //
    // Translation Request APIs
    //

    private static class GetTranslationRequestsResponse extends ServiceResponse {
        Map translationRequests;
    }

    @Override
    public Map getTranslationRequests()
            throws ServiceException {
        GetTranslationRequestsResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/trs",
                null,
                GetTranslationRequestsResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        Map resultTRs = new TreeMap<>();
        for (Entry trEntry : resp.translationRequests.entrySet()) {
            String id = trEntry.getKey();
            RestTranslationRequest tr = trEntry.getValue();
            resultTRs.put(id, new TranslationRequestDataImpl(id, tr));
        }

        return resultTRs;
    }


    private static class TranslationRequestResponse extends ServiceResponse {
        String id;
        RestTranslationRequest translationRequest;
    }

    @Override
    public TranslationRequestData getTranslationRequest(
            String trId) throws ServiceException {
        if (trId == null || trId.isEmpty()) {
            throw new IllegalArgumentException("Non-empty trId must be specified.");
        }

        TranslationRequestResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/trs/"
                    + escapePathSegment(trId),
                null,
                TranslationRequestResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new TranslationRequestDataImpl(resp.id, resp.translationRequest);
    }

    @Override
    public TranslationRequestData createTranslationRequest(
            NewTranslationRequestData newTranslationRequestData)
            throws ServiceException {
        if (newTranslationRequestData == null) {
            throw new IllegalArgumentException("Non-empty newTranslationRequestData must be specified.");
        }

        RestInputTranslationRequestData newRestTRData = new RestInputTranslationRequestData(newTranslationRequestData);
        Gson gson = createGson(RestInputTranslationRequestData.class.getName());
        String jsonBody = gson.toJson(newRestTRData, RestInputTranslationRequestData.class);
        TranslationRequestResponse resp = invokeApiJson(
                "POST",
                escapePathSegment(account.getInstanceId()) + "/v2/trs/new",
                jsonBody,
                TranslationRequestResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new TranslationRequestDataImpl(resp.id, resp.translationRequest);
    }


    @Override
    public TranslationRequestData updateTranslationRequest(
            String trId,
            TranslationRequestDataChangeSet changeSet) throws ServiceException {
        if (trId == null || trId.isEmpty()) {
            throw new IllegalArgumentException("Non-empty trId must be specified.");
        }
        if (changeSet == null) {
            throw new IllegalArgumentException("Non-null changeSet must be specified.");
        }

        RestInputTranslationRequestData restChangeSet = new RestInputTranslationRequestData(changeSet);
        Gson gson = createGson(RestInputTranslationRequestData.class.getName());
        String jsonBody = gson.toJson(restChangeSet, RestInputTranslationRequestData.class);
        TranslationRequestResponse resp = invokeApiJson(
                "POST",
                escapePathSegment(account.getInstanceId()) + "/v2/trs/"
                        + escapePathSegment(trId),
                jsonBody,
                TranslationRequestResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new TranslationRequestDataImpl(resp.id, resp.translationRequest);
    }


    @Override
    public void deleteTranslationRequest(String trId)
            throws ServiceException {
        ServiceResponse resp = invokeApiJson(
                "DELETE",
                escapePathSegment(account.getInstanceId()) + "/v2/trs/"
                    + trId,
                null,
                ServiceResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }
    }


    @Override
    public BundleData getTRBundleInfo(String trId,
            String bundleId) throws ServiceException {
        if (trId == null || trId.isEmpty()) {
            throw new IllegalArgumentException("Non-empty trId must be specified.");
        }
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("Non-empty bundleId must be specified.");
        }

        GetBundleInfoResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/trs/"
                    + escapePathSegment(trId) + "/" + escapePathSegment(bundleId),
                null,
                GetBundleInfoResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new BundleDataImpl(resp.bundle);
    }

    @Override
    public Map getTRResourceEntries(
            String trId, String bundleId, String language)
            throws ServiceException {
        if (trId == null || trId.isEmpty()) {
            throw new IllegalArgumentException("Non-empty trId must be specified.");
        }
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("Non-empty bundleId must be specified.");
        }
        if (language == null || language.isEmpty()) {
            throw new IllegalArgumentException("Non-empty languageId must be specified.");
        }

        GetResourceEntriesResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/trs/"
                    + escapePathSegment(trId) + "/"
                    + escapePathSegment(bundleId) + "/" + language,
                null,
                GetResourceEntriesResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        Map resultEntries = new TreeMap();
        if (resp.resourceEntries != null && !resp.resourceEntries.isEmpty()) {
            for (Entry entry : resp.resourceEntries.entrySet()) {
                resultEntries.put(entry.getKey(),
                        new ResourceEntryDataImpl(entry.getValue()));
            }
        }
        return resultEntries;
    }


    @Override
    public ResourceEntryData getTRResourceEntry(String trId,
            String bundleId, String language, String resKey)
            throws ServiceException {
        if (trId == null || trId.isEmpty()) {
            throw new IllegalArgumentException("Non-empty trId must be specified.");
        }
        if (bundleId == null || bundleId.isEmpty()) {
            throw new IllegalArgumentException("Non-empty bundleId must be specified.");
        }
        if (language == null || language.isEmpty()) {
            throw new IllegalArgumentException("Non-empty language must be specified.");
        }
        if (resKey == null || resKey.isEmpty()) {
            throw new IllegalArgumentException("Non-empty resKey must be specified.");
        }

        GetResourceEntryResponse resp = invokeApiJson(
                "GET",
                escapePathSegment(account.getInstanceId()) + "/v2/bundles/"
                    + escapePathSegment(bundleId) + "/" + language
                    + "/" + escapePathSegment(resKey),
                null,
                GetResourceEntryResponse.class);

        if (resp.getStatus() == Status.ERROR) {
            throw new ServiceException(resp.getMessage());
        }

        return new ResourceEntryDataImpl(resp.resourceEntry);
    }


    //
    // XLIFF APIs
    //

    @Override
    public void getXliffFromBundles(String srcLanguage,
            String trgLanguage, Set bundleIds,
            OutputStream outputXliff) throws ServiceException, IOException {
        if (srcLanguage == null || srcLanguage.isEmpty()) {
            throw new IllegalArgumentException("Non-empty srcLanguage must be specified.");
        }
        if (trgLanguage == null || trgLanguage.isEmpty()) {
            throw new IllegalArgumentException("Non-empty trgLanguage must be specified.");
        }

        StringBuilder urlBuf = new StringBuilder();
        urlBuf
            .append(escapePathSegment(account.getInstanceId()))
            .append("/v2/xliff/bundles/")
            .append(srcLanguage)
            .append("/")
            .append(trgLanguage);

        if (bundleIds != null && !bundleIds.isEmpty()) {
            urlBuf.append("?bundles=");
            boolean first = true;
            for (String bundleId : bundleIds) {
                if (first) {
                    first = false;
                } else {
                    urlBuf.append(",");
                }
                urlBuf.append(bundleId);
            }
        }

        ApiResponse resp = null;
        try {
            resp = invokeApi("GET", urlBuf.toString(), null, null, false);
        } catch (Exception e) {
            String errMsg = "Error while processing API request GET " + urlBuf;
            throw new ServiceException(errMsg, e);
        }
        assert resp != null;

        if (resp.contentType == null || !resp.contentType.equalsIgnoreCase("application/xliff+xml")) {
            throw new ServiceException("Received HTTP status: " + resp.status
                    + " with non-XLIFF response (" + resp.contentType + ") from GET"
                    + " " + urlBuf.toString());
        }
        assert resp.body != null;
        outputXliff.write(resp.body);
    }


    @Override
    public void updateBundlesWithXliff(InputStream inputXliff)
            throws ServiceException, IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buf = new byte[2048];
        int bytes;
        while ((bytes = inputXliff.read(buf)) != -1) {
            baos.write(buf, 0, bytes);
        }
        byte[] inputXliffBytes = baos.toByteArray();

        String method = "POST";
        String apiPath = escapePathSegment(account.getInstanceId())
                + "/v2/xliff/bundles";

        ApiResponse resp = null;
        try {
            resp = invokeApi(method, apiPath, "application/xliff+xml", inputXliffBytes, false);
        } catch (Exception e) {
            String errMsg = "Error while processing API request " + method + " " + apiPath;
            throw new ServiceException(errMsg, e);
        }
        if (resp.status >= 300) {
            String bodyStr = resp.body != null ? new String(resp.body, StandardCharsets.UTF_8) : null;
            throw new ServiceException("Received HTTP status: " + resp.status + " from " + method
                    + " " + apiPath + ", body: " + bodyStr);
        }
    }


    @Override
    public void getXliffFromTranslationRequest(String trId,
            String srcLanguage, String trgLanguage,
            OutputStream outputXliff) throws ServiceException, IOException {
        if (trId == null || trId.isEmpty()) {
            throw new IllegalArgumentException("Non-empty trId must be specified.");
        }
        if (srcLanguage == null || srcLanguage.isEmpty()) {
            throw new IllegalArgumentException("Non-empty srcLanguage must be specified.");
        }
        if (trgLanguage == null || trgLanguage.isEmpty()) {
            throw new IllegalArgumentException("Non-empty trgLanguage must be specified.");
        }

        StringBuilder urlBuf = new StringBuilder();
        urlBuf
            .append(escapePathSegment(account.getInstanceId()))
            .append("/v2/xliff/trs/")
            .append(trId)
            .append("/")
            .append(srcLanguage)
            .append("/")
            .append(trgLanguage);

        ApiResponse resp = null;
        try {
            resp = invokeApi("GET", urlBuf.toString(), null, null, false);
        } catch (Exception e) {
            String errMsg = "Error while processing API request GET " + urlBuf;
            throw new ServiceException(errMsg, e);
        }
        assert resp != null;

        if (resp.contentType == null || !resp.contentType.equalsIgnoreCase("application/xliff+xml")) {
            throw new ServiceException("Received HTTP status: " + resp.status
                    + " with non-XLIFF response (" + resp.contentType + ") from GET"
                    + " " + urlBuf.toString());
        }
        assert resp.body != null;
        outputXliff.write(resp.body);
    }


    //
    // Private method used for calling REST endpoints
    //

    private  T invokeApiJson(String method, String apiPath, String inJson, Class classOfT)
        throws ServiceException {
        return invokeApiJson(method, apiPath, inJson, classOfT, false);
    }

    private  T invokeApiJson(String method, String apiPath, String inJson, Class classOfT,
            boolean anonymous) throws ServiceException {

        T responseObj = null;
        try {
            // Request body in UTF-8
            String contentType = null;
            byte[] requestBody = null;
            if (inJson != null) {
                requestBody = inJson.getBytes(StandardCharsets.UTF_8);
                contentType = "application/json";
            }

            ApiResponse resp = invokeApi(method, apiPath, contentType, requestBody, anonymous);

            if (resp.contentType == null || !resp.contentType.equalsIgnoreCase("application/json")) {
                throw new ServiceException("Received HTTP status: " + resp.status
                        + " with non-JSON response from " + method + " " + apiPath);
            }

            Reader reader = new InputStreamReader(new ByteArrayInputStream(resp.body), StandardCharsets.UTF_8);
            Gson gson = createGson(classOfT.getName());
            responseObj = gson.fromJson(reader, classOfT);
            

        } catch (Exception e) {
            // Error handling
            String errMsg = "Error while processing API request " + method + " " + apiPath;
            throw new ServiceException(errMsg, e);
        }

        return responseObj;
    }
    
    private  T invokeApiInputStream(String method, String apiPath, String contentType, FileInputStream fis, Class classOfT,
            boolean anonymous) throws ServiceException {

        byte[] requestBody = null;
        T responseObj = null;
        try {
            requestBody = ByteStreams.toByteArray((InputStream) fis);
            ApiResponse resp = invokeApi(method, apiPath, contentType, requestBody, anonymous);

            Reader reader = new InputStreamReader(new ByteArrayInputStream(resp.body), StandardCharsets.UTF_8);
            Gson gson = createGson(classOfT.getName());
            responseObj = gson.fromJson(reader, classOfT);

        } catch (Exception e) {
            // Error handling
            String errMsg = "Error while processing API request " + method + " " + apiPath;
            throw new ServiceException(errMsg, e);
        }

        return responseObj;
    }

    private static class ApiResponse {
        int status;
        String contentType;
        byte[] body;
    }

    private ApiResponse invokeApi(String method, String apiPath, String inContentType, byte[] inBody,
            boolean anonymous) throws IOException,TokenManagerException {
        String urlStr = account.getUrl() + "/" + apiPath;
        URL targetUrl = new URL(urlStr);
        HttpURLConnection conn = (HttpURLConnection)targetUrl.openConnection();
        conn.setRequestMethod(method);

        // Date header
        SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
        String dateHeader = sdf.format(new Date());

        conn.setRequestProperty("Date", dateHeader);

        // Authorization header
        if (!anonymous) {
            StringBuilder authHeader = new StringBuilder();
            if (account.isIamEnabled()) {
                authHeader.append("Bearer ");
                authHeader.append(account.getIamToken());
            } else {
                String uid = account.getUserId();
                String secret = account.getPassword();

                switch (scheme) {
                case BASIC:
                    authHeader.append("Basic ");
                    authHeader.append(getBasicCredential(uid, secret));
                    break;

                case HMAC:
                    authHeader.append("GaaS-HMAC ");
                    authHeader.append(getHmacCredential(uid, secret, method,
                            urlStr, dateHeader, inBody));
                    break;
                }
            }
            conn.setRequestProperty("Authorization", authHeader.toString());
        }

        // write the request body
        if (inBody != null) {
            conn.setRequestProperty("Content-Type", inContentType);
            conn.setDoOutput(true);
            try(OutputStream os=conn.getOutputStream()){
                os.write(inBody);
            }
        }

        // receiving response
        ApiResponse resp = new ApiResponse();

        resp.status = conn.getResponseCode();
        resp.contentType = conn.getContentType();

        // response body
        int bodyLen = conn.getContentLength();
        if (bodyLen < 0) {
            bodyLen = 2048; // default length for initial byte array
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream(bodyLen);
        try (InputStream is = conn.getErrorStream() == null
                ? conn.getInputStream()
                : conn.getErrorStream()) {
            byte[] buf = new byte[2048];
            int bytes;
            while ((bytes = is.read(buf)) != -1) {
                baos.write(buf, 0, bytes);
            }

            resp.body = baos.toByteArray();

            return resp;
        }
    }

    private static final char SEP = ':';

    //
    // Basic credential
    //
    private static String getBasicCredential(String uid, String secret) {
        if (uid == null || secret == null) {
            throw new IllegalArgumentException("uid and secrent must not be null");
        }
        String token = uid + ":" + secret;
        try {
            return BaseEncoding.base64().encode(token.getBytes("ISO-8859-1"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //
    // Globalization Pipeline HMAC credential
    //

    private static final byte LINE_SEP = 0x0A;
    private static final String ENC = "ISO-8859-1";
    private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";

    private static String getHmacCredential(String uid, String secret, String method, String url,
            String rfc1123Date, byte[] body) {
        if (uid == null || method == null || url == null || rfc1123Date == null) {
            throw new IllegalArgumentException("uid, secret, method, url and rfc1123Date must not be null");
        }

        StringBuilder credential = new StringBuilder(uid);
        credential.append(SEP);

        try {
            // Actual secret used by GaaS looks like: "zg5SlD+ftXYRIZDblLgEA/ILkkCNqE1y"
            // This is actually a base64 encoded random bytes. Although we could
            // get original random bytes by decoding base64, but we don't do it because
            // it can be any 'String' in future. We simply get byte[] expression of the
            // secret 'String' (which is restricted to a subset of US-ASCII).
            SecretKeySpec key = new SecretKeySpec(secret.getBytes(ENC), HMAC_SHA1_ALGORITHM);
            Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
            mac.init(key);

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            baos.write(method.getBytes(ENC));
            baos.write(LINE_SEP);
            baos.write(url.getBytes(ENC));
            baos.write(LINE_SEP);
            baos.write(rfc1123Date.getBytes(ENC));
            baos.write(LINE_SEP);
            if (body != null) {
                baos.write(body);
            }
            byte[] msg = baos.toByteArray();

            // signing
            byte[] hmac = mac.doFinal(msg);
            credential.append(BaseEncoding.base64().encode(hmac));
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException(e);
        }

        return credential.toString();
    }

    //
    // Custom JSON serialization/deserialization supporting Java Enum
    //

    // Custom type adapter for serializing/deserializing Java enum with mapping.
    // This method assumes all enum constants are in upper case.
    static class EnumWithFallbackAdapter> extends TypeAdapter {
        private final T fallback;

        public EnumWithFallbackAdapter(T fallback) {
            this.fallback = fallback;
        }

        T toEnumWithFallback(String s) {
            if (s == null) {
                return null;
            }

            T value = null;
            try {
                value = (T) Enum.valueOf(fallback.getDeclaringClass(), s.toUpperCase(Locale.ROOT));
            } catch (IllegalArgumentException e) {
                // fallback
                value = fallback;
            }
            return value;
        }

        @Override
        public T read(JsonReader in) throws IOException {
            if (in.peek() == JsonToken.NULL) {
                in.nextNull();
                return null;
            }
            return toEnumWithFallback(in.nextString());
        }

        @Override
        public void write(JsonWriter out, T value) throws IOException {
            if (value == null) {
                out.nullValue();
                return;
            }
            out.value(value.name());
        }
    }

    private static class EnumMapInstanceCreator, V>
            implements InstanceCreator> {
        private final Class enumClazz;

        public EnumMapInstanceCreator(final Class enumClazz) {
            super();
            this.enumClazz = enumClazz;
        }

        @Override
        public EnumMap createInstance(final Type type) {
            return new EnumMap(enumClazz);
        }
    }

    // TypeAdapterFactory used for serializing map entries with null value.
    // updateResourceStrings and some other REST APIs handles properties
    // with null value as deletion directive.
    static class NullMapValueTypeAdapterFactory implements TypeAdapterFactory {
        @Override
        public  TypeAdapter create(Gson gson, TypeToken typeToken) {
            // The special handling is only applicable to Map objects.
            Class rawType = typeToken.getRawType();
            if (!Map.class.isAssignableFrom(rawType)) {
                return null;
            }

            final TypeAdapter delegate = gson.getDelegateAdapter(this, typeToken);
            return new TypeAdapter() {
                public void write(JsonWriter out, T value) throws IOException {
                    boolean serializeNulls = out.getSerializeNulls();
                    if (!serializeNulls) {
                        // force null serialization
                        out.setSerializeNulls(true);
                    }
                    delegate.write(out, value);
                    if (!serializeNulls) {
                        // reset
                        out.setSerializeNulls(false);
                    }
                }
                public T read(JsonReader in) throws IOException {
                    return delegate.read(in);
                }
            };
        }
    }
    /**
     * Creates a new Gson object
     * 
     * @param className A class name used for serialization/deserialization.
     *                  

Note: This implementation does not use this argument * for now. If we need different kinds of type adapters * depending on class, the implementation might be updated * to set up appropriate set of type adapters. * @return A Gson object */ private static Gson createGson(String className) { GsonBuilder builder = new GsonBuilder(); // ISO8601 date format support builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"); builder.registerTypeAdapter(TranslationStatus.class, new EnumWithFallbackAdapter(TranslationStatus.UNKNOWN)); builder.registerTypeAdapter(TranslationRequestStatus.class, new EnumWithFallbackAdapter(TranslationRequestStatus.UNKNOWN)); builder.registerTypeAdapter( new TypeToken>() {}.getType(), new EnumMapInstanceCreator(TranslationStatus.class)); builder.registerTypeAdapterFactory(new NullMapValueTypeAdapterFactory()); return builder.create(); } private static String escapePathSegment(String pathSegment) { return UrlEscapers.urlPathSegmentEscaper().escape(pathSegment); } //Document Translation Request APIs private static class GetDocumentTranslationRequestsResponse extends ServiceResponse { Map translationRequests; } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#getDocumentTranslationRequests() */ @Override public Map getDocumentTranslationRequests() throws ServiceException { GetDocumentTranslationRequestsResponse resp = invokeApiJson( "GET", escapePathSegment(account.getInstanceId()) + "/v2/doc-trs", null, GetDocumentTranslationRequestsResponse.class); if (resp.getStatus() == Status.ERROR) { throw new ServiceException(resp.getMessage()); } Map resultTRs = new TreeMap<>(); for (Entry trEntry : resp.translationRequests.entrySet()) { String id = trEntry.getKey(); RestDocumentTranslationRequest tr = trEntry.getValue(); resultTRs.put(id, new DocumentTranslationRequestDataImpl(id, tr)); } return resultTRs; } private static class DocumentTranslationRequestResponse extends ServiceResponse { String id; RestDocumentTranslationRequest translationRequest; } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#getDocumentTranslationRequest(java.lang.String) */ @Override public DocumentTranslationRequestData getDocumentTranslationRequest( String trId) throws ServiceException { if (trId == null || trId.isEmpty()) { throw new IllegalArgumentException("Non-empty trId must be specified."); } DocumentTranslationRequestResponse resp = invokeApiJson( "GET", escapePathSegment(account.getInstanceId()) + "/v2/doc-trs/" + escapePathSegment(trId), null, DocumentTranslationRequestResponse.class); if (resp.getStatus() == Status.ERROR) { throw new ServiceException(resp.getMessage()); } return new DocumentTranslationRequestDataImpl(resp.id, resp.translationRequest); } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#createDocumentTranslationRequest(com.ibm.g11n.pipeline.client.NewDocumentTranslationRequestData) */ @Override public DocumentTranslationRequestData createDocumentTranslationRequest( NewDocumentTranslationRequestData newTranslationRequestData) throws ServiceException { if (newTranslationRequestData == null) { throw new IllegalArgumentException("Non-empty newTranslationRequestData must be specified."); } RestInputDocumentTranslationRequestData newRestTRData = new RestInputDocumentTranslationRequestData(newTranslationRequestData); Gson gson = createGson(RestInputDocumentTranslationRequestData.class.getName()); String jsonBody = gson.toJson(newRestTRData, RestInputDocumentTranslationRequestData.class); DocumentTranslationRequestResponse resp = invokeApiJson( "POST", escapePathSegment(account.getInstanceId()) + "/v2/doc-trs/new", jsonBody, DocumentTranslationRequestResponse.class); if (resp.getStatus() == Status.ERROR) { throw new ServiceException(resp.getMessage()); } return new DocumentTranslationRequestDataImpl(resp.id, resp.translationRequest); } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#updateDocumentTranslationRequest(java.lang.String, com.ibm.g11n.pipeline.client.DocumentTranslationRequestDataChangeSet) */ @Override public DocumentTranslationRequestData updateDocumentTranslationRequest( String trId, DocumentTranslationRequestDataChangeSet changeSet) throws ServiceException { if (trId == null || trId.isEmpty()) { throw new IllegalArgumentException("Non-empty trId must be specified."); } if (changeSet == null) { throw new IllegalArgumentException("Non-null changeSet must be specified."); } RestInputDocumentTranslationRequestData restChangeSet = new RestInputDocumentTranslationRequestData(changeSet); Gson gson = createGson(RestInputDocumentTranslationRequestData.class.getName()); String jsonBody = gson.toJson(restChangeSet, RestInputDocumentTranslationRequestData.class); DocumentTranslationRequestResponse resp = invokeApiJson( "POST", escapePathSegment(account.getInstanceId()) + "/v2/doc-trs/" + escapePathSegment(trId), jsonBody, DocumentTranslationRequestResponse.class); if (resp.getStatus() == Status.ERROR) { throw new ServiceException(resp.getMessage()); } return new DocumentTranslationRequestDataImpl(resp.id, resp.translationRequest); } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#deleteDocumentTranslationRequest(java.lang.String) */ @Override public void deleteDocumentTranslationRequest(String trId) throws ServiceException { ServiceResponse resp = invokeApiJson( "DELETE", escapePathSegment(account.getInstanceId()) + "/v2/doc-trs/" + trId, null, ServiceResponse.class); if (resp.getStatus() == Status.ERROR) { throw new ServiceException(resp.getMessage()); } } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#getTRDocumentInfo(java.lang.String, com.ibm.g11n.pipeline.client.DocumentType, java.lang.String) */ @Override public DocumentData getTRDocumentInfo(String trId, DocumentType type, String documentId) throws ServiceException { if (trId == null || trId.isEmpty()) { throw new IllegalArgumentException("Non-empty trId must be specified."); } if (documentId == null || documentId.isEmpty()) { throw new IllegalArgumentException("Non-empty documentId must be specified."); } GetDocumentInfoResponse resp = invokeApiJson( "GET", escapePathSegment(account.getInstanceId()) + "/v2/doc-trs/" + escapePathSegment(trId) + "/" + type.toString().toLowerCase() + "/" + escapePathSegment(documentId), null, GetDocumentInfoResponse.class); if (resp.getStatus() == Status.ERROR) { throw new ServiceException(resp.getMessage()); } return new DocumentDataImpl(resp.documentData); } private static class GetSegmentsResponse extends ServiceResponse { Map segments; } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#getTRSegments(java.lang.String, com.ibm.g11n.pipeline.client.DocumentType, java.lang.String, java.lang.String) */ @Override public Map getTRSegments(String trId, DocumentType type, String documentId, String language) throws ServiceException { if (trId == null || trId.isEmpty()) { throw new IllegalArgumentException("Non-empty trId must be specified."); } if (documentId == null || documentId.isEmpty()) { throw new IllegalArgumentException("Non-empty documentId must be specified."); } if (language == null || language.isEmpty()) { throw new IllegalArgumentException("Non-empty languageId must be specified."); } GetSegmentsResponse resp = invokeApiJson( "GET", escapePathSegment(account.getInstanceId()) + "/v2/doc-trs/" + escapePathSegment(trId) + "/" + type.toString().toLowerCase() + "/" + escapePathSegment(documentId) + "/" + language, null, GetSegmentsResponse.class); if (resp.getStatus() == Status.ERROR) { throw new ServiceException(resp.getMessage()); } Map resultEntries = new TreeMap(); if (resp.segments != null && !resp.segments.isEmpty()) { for (Entry entry : resp.segments.entrySet()) { resultEntries.put(entry.getKey(), new SegmentDataImpl(entry.getValue())); } } return resultEntries; } private static class GetSegmentResponse extends ServiceResponse { RestSegmentData segmentData; } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#getTRSegment(java.lang.String, com.ibm.g11n.pipeline.client.DocumentType, java.lang.String, java.lang.String, java.lang.String) */ @Override public SegmentData getTRSegment(String trId, DocumentType type, String documentId, String language, String segmentKey) throws ServiceException { if (trId == null || trId.isEmpty()) { throw new IllegalArgumentException("Non-empty trId must be specified."); } if (documentId == null || documentId.isEmpty()) { throw new IllegalArgumentException("Non-empty documentId must be specified."); } if (language == null || language.isEmpty()) { throw new IllegalArgumentException("Non-empty language must be specified."); } if (segmentKey == null || segmentKey.isEmpty()) { throw new IllegalArgumentException("Non-empty segmentKey must be specified."); } GetSegmentResponse resp = invokeApiJson( "GET", escapePathSegment(account.getInstanceId()) + "/v2/doc-trs/" + escapePathSegment(trId) + "/" + type.toString().toLowerCase() + "/" + escapePathSegment(documentId) + "/" + language + "/" + escapePathSegment(segmentKey), null, GetSegmentResponse.class); if (resp.getStatus() == Status.ERROR) { throw new ServiceException(resp.getMessage()); } return new SegmentDataImpl(resp.segmentData); } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#getXliffFromDocuments(java.lang.String, java.lang.String, java.util.Map, java.io.OutputStream) */ @Override public void getXliffFromDocuments(String srcLanguage, String trgLanguage, Map> documentsMap, OutputStream outputXliff) throws ServiceException, IOException { if (srcLanguage == null || srcLanguage.isEmpty()) { throw new IllegalArgumentException("Non-empty srcLanguage must be specified."); } if (trgLanguage == null || trgLanguage.isEmpty()) { throw new IllegalArgumentException("Non-empty trgLanguage must be specified."); } StringBuilder urlBuf = new StringBuilder(); urlBuf .append(escapePathSegment(account.getInstanceId())) .append("/v2/doc-xliff/") .append(srcLanguage) .append("/") .append(trgLanguage); if (documentsMap != null && !documentsMap.isEmpty()) { urlBuf.append("?"); int count = documentsMap.size(); for (Map.Entry> entry : documentsMap.entrySet()) { DocumentType type = entry.getKey(); Set documentIds = entry.getValue(); urlBuf.append(type.toString().toLowerCase()).append("="); boolean first = true; for (String docId : documentIds) { if (first) { first = false; } else { urlBuf.append(","); } urlBuf.append(docId); } count--; } if (count != 0) { urlBuf.append("&"); } } ApiResponse resp = null; try { resp = invokeApi("GET", urlBuf.toString(), null, null, false); } catch (Exception e) { String errMsg = "Error while processing API request GET " + urlBuf; throw new ServiceException(errMsg, e); } assert resp != null; if (resp.contentType == null || !resp.contentType.equalsIgnoreCase("application/xliff+xml")) { throw new ServiceException("Received HTTP status: " + resp.status + " with non-XLIFF response (" + resp.contentType + ") from GET" + " " + urlBuf.toString()); } assert resp.body != null; outputXliff.write(resp.body); } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#updateDocumentsWithXliff(java.io.InputStream) */ @Override public void updateDocumentsWithXliff(InputStream inputXliff) throws ServiceException, IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[2048]; int bytes; while ((bytes = inputXliff.read(buf)) != -1) { baos.write(buf, 0, bytes); } byte[] inputXliffBytes = baos.toByteArray(); String method = "POST"; String apiPath = escapePathSegment(account.getInstanceId()) + "/v2/doc-xliff"; ApiResponse resp = null; try { resp = invokeApi(method, apiPath, "application/xliff+xml", inputXliffBytes, false); } catch (Exception e) { String errMsg = "Error while processing API request " + method + " " + apiPath; throw new ServiceException(errMsg, e); } if (resp.status >= 300) { String bodyStr = resp.body != null ? new String(resp.body, StandardCharsets.UTF_8) : null; throw new ServiceException("Received HTTP status: " + resp.status + " from " + method + " " + apiPath + ", body: " + bodyStr); } } /* (non-Javadoc) * @see com.ibm.g11n.pipeline.client.ServiceClient#getXliffFromDocumentTranslationRequest(java.lang.String, java.lang.String, java.lang.String, java.io.OutputStream) */ @Override public void getXliffFromDocumentTranslationRequest(String trId, String srcLanguage, String trgLanguage, OutputStream outputXliff) throws ServiceException, IOException { if (trId == null || trId.isEmpty()) { throw new IllegalArgumentException("Non-empty trId must be specified."); } if (srcLanguage == null || srcLanguage.isEmpty()) { throw new IllegalArgumentException("Non-empty srcLanguage must be specified."); } if (trgLanguage == null || trgLanguage.isEmpty()) { throw new IllegalArgumentException("Non-empty trgLanguage must be specified."); } StringBuilder urlBuf = new StringBuilder(); urlBuf .append(escapePathSegment(account.getInstanceId())) .append("/v2/doc-xliff/trs/") .append(trId) .append("/") .append(srcLanguage) .append("/") .append(trgLanguage); ApiResponse resp = null; try { resp = invokeApi("GET", urlBuf.toString(), null, null, false); } catch (Exception e) { String errMsg = "Error while processing API request GET " + urlBuf; throw new ServiceException(errMsg, e); } assert resp != null; if (resp.contentType == null || !resp.contentType.equalsIgnoreCase("application/xliff+xml")) { throw new ServiceException("Received HTTP status: " + resp.status + " with non-XLIFF response (" + resp.contentType + ") from GET" + " " + urlBuf.toString()); } assert resp.body != null; outputXliff.write(resp.body); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy