View Javadoc
1   package fr.ifremer.quadrige2.synchro.service.client;
2   
3   /*-
4    * #%L
5    * Quadrige2 Core :: Quadrige2 Client Core
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2017 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   * 
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   * 
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import com.google.common.base.Preconditions;
27  import com.google.gson.reflect.TypeToken;
28  import fr.ifremer.common.synchro.service.SynchroResult;
29  import fr.ifremer.quadrige2.core.config.Quadrige2Configuration;
30  import fr.ifremer.quadrige2.core.dao.technical.gson.Gsons;
31  import fr.ifremer.quadrige2.core.dao.technical.http.HttpHelper;
32  import fr.ifremer.quadrige2.core.exception.*;
33  import fr.ifremer.quadrige2.core.security.AuthenticationInfo;
34  import fr.ifremer.quadrige2.core.service.RestServiceSupport;
35  import fr.ifremer.quadrige2.core.vo.administration.program.ProgramVO;
36  import fr.ifremer.quadrige2.core.vo.administration.user.QuserVO;
37  import fr.ifremer.quadrige2.core.vo.data.survey.CampaignVO;
38  import fr.ifremer.quadrige2.core.vo.system.rule.RuleListVO;
39  import fr.ifremer.quadrige2.synchro.vo.SynchroExportContextVO;
40  import fr.ifremer.quadrige2.synchro.vo.SynchroImportContextVO;
41  import fr.ifremer.quadrige2.synchro.vo.SynchroProgressionStatus;
42  import fr.ifremer.quadrige2.synchro.vo.SynchroProgressionVO;
43  import org.apache.commons.collections4.CollectionUtils;
44  import org.apache.commons.io.FileUtils;
45  import org.apache.commons.io.FilenameUtils;
46  import org.apache.commons.lang3.StringUtils;
47  import org.apache.commons.lang3.time.DateUtils;
48  import org.apache.commons.logging.Log;
49  import org.apache.commons.logging.LogFactory;
50  import org.apache.http.client.CredentialsProvider;
51  import org.apache.http.client.methods.HttpGet;
52  import org.apache.http.client.methods.HttpPost;
53  import org.apache.http.entity.ContentType;
54  import org.apache.http.entity.StringEntity;
55  import org.apache.http.entity.mime.MultipartEntityBuilder;
56  import org.apache.http.impl.client.CloseableHttpClient;
57  import org.apache.http.impl.client.HttpClients;
58  import org.apache.http.protocol.HTTP;
59  import org.nuiton.i18n.I18n;
60  import org.nuiton.jaxx.application.type.ApplicationProgressionModel;
61  import org.nuiton.version.Version;
62  import org.nuiton.version.Versions;
63  import org.springframework.beans.factory.annotation.Autowired;
64  import org.springframework.context.annotation.Lazy;
65  import org.springframework.stereotype.Service;
66  
67  import java.io.File;
68  import java.io.IOException;
69  import java.lang.reflect.Type;
70  import java.util.ArrayList;
71  import java.util.Collection;
72  import java.util.List;
73  import java.util.Set;
74  
75  import static org.nuiton.i18n.I18n.t;
76  
77  /**
78   * <p>
79   * SynchroRestClientServiceImpl class.
80   * </p>
81   * 
82   */
83  @Service("synchroRestClientService")
84  @Lazy
85  public class SynchroRestClientServiceImpl extends RestServiceSupport implements SynchroRestClientService {
86  
87  	private static final Log log = LogFactory.getLog(SynchroRestClientServiceImpl.class);
88  
89  	private static final String URL_USER_CURRENT = "/service/user/current";
90  	private static final String URL_USER_PROGRAMS = "/service/user/programs";
91  	private static final String URL_PROGRAM_GET = "/service/program/get/%s";
92  	private final static String URL_CHECK_VERSION = "/service/version/check/%s";
93  	private final static String URL_GET_VERSION = "/service/version/get";
94  	private final static String URL_IMPORT_CHECK_ANONYMOUSLY = "/service/import/check/anonymous";
95  	private final static String URL_IMPORT_CHECK = "/service/import/check";
96  	private final static String URL_IMPORT_STATUS = "/service/import/status/%s";
97  	private final static String URL_IMPORT_START = "/service/import/start";
98  	private final static String URL_IMPORT_START_ANONYMOUSLY = "/service/import/start/anonymous";
99  	private final static String URL_IMPORT_STOP = "/service/import/stop/%s";
100 	private final static String URL_IMPORT_ACKNOWLEDGE = "/service/import/acknowledge";
101 	private static final String URL_EXPORT_UPLOAD = "/service/export/upload";
102 	private static final String URL_EXPORT_UPLOAD_POST_PARAM_FILE = "file";
103 	private final static String URL_EXPORT_STATUS = "/service/export/status/%s";
104 	private final static String URL_EXPORT_START = "/service/export/start";
105 	private final static String URL_EXPORT_STOP = "/service/export/stop/%s";
106 	private final static String URL_EXPORT_FILES = "/service/export/files/%s";
107 	private static final String URL_EXPORT_ACKNOWLEDGE = "/service/export/acknowledge";
108 	private static final String URL_PROGRAM_SAVE_LIST = "/service/program/save/list/";
109 	private static final String URL_CAMPAIGN_SAVE_LIST = "/service/campaign/save/list/";
110 	private static final String URL_CAMPAIGN_DELETE_LIST = "/service/campaign/delete/list/";
111 	private static final String URL_RULE_LIST_SAVE_LIST = "/service/ruleList/save/list/";
112 	private static final String URL_RULE_LIST_DELETE_LIST = "/service/ruleList/delete/list/";
113 
114 	/**
115 	 * <p>
116 	 * Constructor for SynchroRestClientServiceImpl.
117 	 * </p>
118 	 * 
119 	 * @param config
120 	 *            a {@link fr.ifremer.quadrige2.core.config.Quadrige2Configuration} object.
121 	 */
122 	@Autowired
123 	public SynchroRestClientServiceImpl(Quadrige2Configuration config) {
124 		super(config);
125 	}
126 
127 	/** {@inheritDoc} */
128 	@Override
129 	public CloseableHttpClient getHttpClient(AuthenticationInfo authenticationInfo) {
130 
131 		// get credentials provider
132 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
133 
134 		return HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
135 				.setDefaultCredentialsProvider(credentialsProvider).build();
136 	}
137 
138 	/** {@inheritDoc} */
139 	@Override
140 	public QuserVO getUser(AuthenticationInfo authenticationInfo) {
141 
142 		QuserVO result = null;
143 
144 		if (log.isDebugEnabled()) {
145 			log.debug(String.format("Try to retrieve authenticated user data, from the synchronization server [%s]", getRemoteUrl()));
146 		}
147 
148 		// get credentials provider
149 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
150 
151 		try {
152 			// build http client
153 			try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
154 					.setDefaultCredentialsProvider(credentialsProvider).build()) {
155 
156 				HttpGet request = new HttpGet(getPathUri(URL_USER_CURRENT));
157 				result = HttpHelper.executeRequest(httpClient, request, getGson(), QuserVO.class);
158 			}
159 		} catch (Quadrige2TechnicalException e) {
160 			log.error(I18n.t("quadrige2.error.remote.currentPerson.failed", e.getMessage()), e);
161 			throw e;
162 		} catch (IOException e) {
163 			log.error(I18n.t("quadrige2.error.remote.currentPerson.failed", e.getMessage()), e);
164 			throw new Quadrige2TechnicalException(e);
165 		}
166 
167 		return result;
168 	}
169 
170 	/** {@inheritDoc} */
171 	@Override
172 	public List<ProgramVO> getWritableProgramsForUser(AuthenticationInfo authenticationInfo) {
173 
174 		List<ProgramVO> result = null;
175 
176 		if (log.isDebugEnabled()) {
177 			log.debug(String.format("Try to retrieve programs of authenticated user, from the synchronization server [%s]", getRemoteUrl()));
178 		}
179 
180 		// get credentials provider
181 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
182 
183 		try {
184 			// build http client
185 			try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
186 					.setDefaultCredentialsProvider(credentialsProvider).build()) {
187 
188 				HttpGet request = new HttpGet(getPathUri(URL_USER_PROGRAMS));
189 				Type listType = new TypeToken<ArrayList<ProgramVO>>() {
190 				}.getType();
191 				result = HttpHelper.executeRequest(httpClient, request, getGson(), listType);
192 			}
193 		} catch (Quadrige2TechnicalException e) {
194 			log.error(I18n.t("quadrige2.error.remote.programs.get.failed", e.getMessage()), e);
195 			throw e;
196 		} catch (IOException e) {
197 			log.error(I18n.t("quadrige2.error.remote.programs.get.failed", e.getMessage()), e);
198 			throw new Quadrige2TechnicalException(e);
199 		}
200 
201 		return result;
202 	}
203 
204 	/** {@inheritDoc} */
205 	@Override
206 	public ProgramVO getProgramByCode(AuthenticationInfo authenticationInfo, String progCd) {
207 		Preconditions.checkNotNull(progCd);
208 
209 		if (log.isDebugEnabled()) {
210 			log.debug(String.format("Try to retrieve program [%s], from the synchronization server [%s]", progCd, getRemoteUrl()));
211 		}
212 
213 		// get credentials provider
214 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
215 
216 		try {
217 			// build http client
218 			try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
219 					.setDefaultCredentialsProvider(credentialsProvider).build()) {
220 
221 				HttpGet request = new HttpGet(getPathUri(String.format(URL_PROGRAM_GET, progCd)));
222 				return HttpHelper.executeRequest(httpClient, request, getGson(), ProgramVO.class);
223 			}
224 		} catch (Quadrige2TechnicalException e) {
225 			log.error(I18n.t("quadrige2.error.remote.program.get.failed", progCd, e.getMessage()), e);
226 			throw e;
227 		} catch (IOException e) {
228 			log.error(I18n.t("quadrige2.error.remote.program.get.failed", progCd, e.getMessage()), e);
229 			throw new Quadrige2TechnicalException(e);
230 		}
231 	}
232 
233 	/** {@inheritDoc} */
234 	@Override
235 	public SynchroImportContextVO checkImportContext(AuthenticationInfo authenticationInfo, SynchroImportContextVO importContextVO) {
236 		if (log.isDebugEnabled()) {
237 			log.debug(String.format("Check if exists referential updates on the synchronization server [%s]", getRemoteUrl()));
238 		}
239 
240 		// Limit programs, if set in config file (if not, server will use his configuration)
241 		Set<String> programCodes = config.getSynchroProgramCodeIncludes();
242 		if (CollectionUtils.isNotEmpty(programCodes)) {
243 			importContextVO.setReferentialProgramCodes(programCodes);
244 		}
245 
246 		// Make sure at least one type (referential or data) is set to true
247 		// This is need to avoid a server error (something like "invalid context - nothing to synchronize")
248 		if (!importContextVO.isWithData() && !importContextVO.isWithReferential()) {
249 			importContextVO.setWithReferential(true);
250 		}
251 
252 		// get credentials provider
253 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
254 		boolean isAnonymous = credentialsProvider == null;
255 
256 		try {
257 			// build http client
258 			try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
259 					.setDefaultCredentialsProvider(credentialsProvider)
260 					.build()) {
261 
262 				// Check compatibility with server
263 				checkVersion(httpClient);
264 
265 				// prepare the request
266 				HttpPost request = createHttpPostWithStringEntity(isAnonymous ? URL_IMPORT_CHECK_ANONYMOUSLY : URL_IMPORT_CHECK, importContextVO);
267 
268 				// execute request
269 				return HttpHelper.executeRequest(httpClient, request, getGson(), SynchroImportContextVO.class);
270 			}
271 		} catch (Quadrige2TechnicalException e) {
272 			log.error(I18n.t("quadrige2.error.remote.checkImportContext.failed", e.getMessage()), e);
273 			throw e;
274 		} catch (IOException e) {
275 			log.error(I18n.t("quadrige2.error.remote.checkImportContext.failed", e.getMessage()), e);
276 			throw new Quadrige2TechnicalException(e);
277 		}
278 	}
279 
280 	/** {@inheritDoc} */
281 	@Override
282 	public void checkVersion(AuthenticationInfo authenticationInfo) {
283 
284 		// get credentials provider
285 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
286 
287 		try {
288 			// build http client
289 			try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
290 					.setDefaultCredentialsProvider(credentialsProvider).build()) {
291 
292 				// Check is this version is allowed by server
293 				checkVersion(httpClient);
294 			}
295 
296 		} catch (Quadrige2TechnicalException e) {
297 			log.error(t("quadrige2.error.remote.checkVersion.failed", e.getMessage()), e);
298 			throw e;
299 		} catch (IOException e) {
300 			log.error(t("quadrige2.error.remote.checkVersion.failed", e.getMessage()), e);
301 			throw new Quadrige2TechnicalException(e);
302 		}
303 	}
304 
305 	/** {@inheritDoc} */
306 	@Override
307 	public void checkVersion(CloseableHttpClient httpClient) {
308 
309 		// Read local version (from quadrige2, not the schemaVersionIfUpdate - see mantis #25340)
310 		Version localVersionToCheck = Quadrige2Configuration.getInstance().getVersion();
311 
312 		if (log.isDebugEnabled()) {
313 			log.debug(String.format("Checking if version [%s] is compatible with the synchronization server [%s]", localVersionToCheck.getVersion(),
314 					getRemoteUrl()));
315 		}
316 
317 		// get the version of the server
318 		HttpGet request = new HttpGet(getPathUri(String.format(URL_CHECK_VERSION, localVersionToCheck.getVersion())));
319 		String isVersionAllowedStr = HttpHelper.executeRequest(httpClient, request, getGson(), String.class);
320 		boolean isVersionAllowed = Boolean.valueOf(isVersionAllowedStr);
321 
322 		// If version not allowed by server
323 		if (!isVersionAllowed) {
324 
325 			// get the version of the server
326 			request = new HttpGet(getPathUri(URL_GET_VERSION));
327 			String serverVersionStr = HttpHelper.executeRequest(httpClient, request, getGson(), String.class);
328 			Version serverVersion = null;
329 			if (StringUtils.isNotBlank(serverVersionStr)) {
330 				serverVersion = Versions.valueOf(serverVersionStr.replaceAll("\\\"", ""));
331 			}
332 
333 			// Fail with an error message
334 			throw new Quadrige2BusinessException(I18n.t("quadrige2.error.synchro.version", localVersionToCheck, serverVersion));
335 		}
336 	}
337 
338 	/** {@inheritDoc} */
339 	@Override
340 	public SynchroProgressionVO startImport(AuthenticationInfo authenticationInfo,
341 			SynchroImportContextVO importContextVO,
342 			ApplicationProgressionModel progressionModel) throws Exception {
343 		Preconditions.checkNotNull(importContextVO);
344 
345 		int refreshTimeout = config.getSynchronizationRefreshTimeout();
346 		String jobId = importContextVO.getJobId();
347 		boolean previouslyLaunched = jobId != null;
348 
349 		// get credentials provider
350 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
351 		boolean isAnonymous = credentialsProvider == null;
352 
353 		try {
354 			// build http client
355 			try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
356 					.setDefaultCredentialsProvider(credentialsProvider)
357 					.build()) {
358 
359 				progressionModel.setMessage(t("quadrige2.synchro.progress.start"));
360 
361 				// check version compatibility
362 				checkVersion(httpClient);
363 
364 				if (!previouslyLaunched) {
365 					// prepare the request
366 					HttpPost request = createHttpPostWithStringEntity(isAnonymous ? URL_IMPORT_START_ANONYMOUSLY : URL_IMPORT_START, importContextVO);
367 
368 					// execute request
369 					SynchroImportContextVO resultImportContextVO = HttpHelper.executeRequest(httpClient, request, getGson(),
370 							SynchroImportContextVO.class);
371 					jobId = resultImportContextVO.getJobId();
372 
373 					// refresh source import context
374 					importContextVO.setJobId(jobId);
375 				}
376 
377 				// wait a while
378 				Thread.sleep(refreshTimeout);
379 
380 				return getImportLastStatus(httpClient, jobId, progressionModel);
381 			}
382 		} catch (Quadrige2TechnicalException e) {
383 			log.error(I18n.t("quadrige2.error.remote.startImport.failed", e.getMessage()), e);
384 			throw e;
385 		} catch (IOException e) {
386 			log.error(I18n.t("quadrige2.error.remote.startImport.failed", e.getMessage()), e);
387 			throw new Quadrige2TechnicalException(e);
388 		}
389 	}
390 
391 	/** {@inheritDoc} */
392 	@Override
393 	public void stopImport(AuthenticationInfo authenticationInfo,
394 			String importJobId) throws Exception {
395 		if (log.isDebugEnabled()) {
396 			log.debug(String.format("try to stop synchronization job [%s] on servers", importJobId));
397 		}
398 
399 		// get credentials provider
400 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
401 
402 		// build http client
403 		try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
404 				.setDefaultCredentialsProvider(credentialsProvider)
405 				.build()) {
406 
407 			// prepare the request
408 			HttpGet request = new HttpGet(getPathUri(String.format(URL_IMPORT_STOP, importJobId)));
409 
410 			// execute stop request
411 			HttpHelper.executeRequest(httpClient, request);
412 
413 		}
414 	}
415 
416 	/** {@inheritDoc} */
417 	@Override
418 	public void acknowledgeImport(AuthenticationInfo authenticationInfo, SynchroImportContextVO importContextVO) throws Quadrige2TechnicalException {
419 
420 		if (log.isDebugEnabled()) {
421 			log.debug(String.format("Sending import acknowledge to the synchronization server [%s]", getRemoteUrl()));
422 		}
423 
424 		// Remove the extra offset from referenretial date (added from SynchroClientService)
425 		if (importContextVO.getReferentialUpdateDate() != null) {
426 			importContextVO = (SynchroImportContextVO) importContextVO.clone();
427 			importContextVO.setReferentialUpdateDate(
428 					DateUtils.addSeconds(importContextVO.getReferentialUpdateDate(),
429 							-1 * config.getImportReferentialUpdateDateOffsetInSecond()));
430 		}
431 
432 		// get credentials provider
433 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
434 
435 		try {
436 			// build http client
437 			try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
438 					.setDefaultCredentialsProvider(credentialsProvider).build()) {
439 
440 				// prepare the request
441 				HttpPost request = createHttpPostWithStringEntity(URL_IMPORT_ACKNOWLEDGE, importContextVO);
442 
443 				// execute request
444 				HttpHelper.executeRequest(httpClient, request);
445 			}
446 		} catch (Quadrige2TechnicalException e) {
447 			log.error(I18n.t("quadrige2.error.remote.acknowledgeImport.failed", e.getMessage()), e);
448 			// If error, be quiet ! (don't care if server could not delete its files)
449 		} catch (IOException e) {
450 			log.error(I18n.t("quadrige2.error.remote.acknowledgeImport.failed", e.getMessage()), e);
451 			throw new Quadrige2TechnicalException(e);
452 		}
453 	}
454 
455 	/** {@inheritDoc} */
456 	@Override
457 	public void uploadExportFile(AuthenticationInfo authenticationInfo, File fileToUpload, ApplicationProgressionModel progressionModel)
458 			throws Quadrige2TechnicalException {
459 
460 		if (!fileToUpload.exists()) {
461 			throw new Quadrige2BusinessException(I18n.t("quadrige2.error.file.not.exists", fileToUpload.getAbsolutePath()));
462 		}
463 
464 		if (log.isDebugEnabled()) {
465 			log.debug(String.format("file to transfert to server: %s", fileToUpload.getAbsolutePath()));
466 		}
467 
468 		// get credentials provider
469 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
470 
471 		try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
472 				.setDefaultCredentialsProvider(credentialsProvider)
473 				.build()) {
474 
475 			// prepare the request
476 			HttpPost uploadHttpPost = new HttpPost(getPathUri(URL_EXPORT_UPLOAD));
477 
478 			// add file
479 			MultipartEntityBuilder builder = MultipartEntityBuilder.create();
480 			// builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
481 			builder.addPart(URL_EXPORT_UPLOAD_POST_PARAM_FILE, new ProgressFileBody(fileToUpload, progressionModel));
482 
483 			uploadHttpPost.setEntity(builder.build());
484 
485 			uploadHttpPost.addHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
486 
487 			// execute request
488 			HttpHelper.executeRequest(httpClient, uploadHttpPost);
489 
490 		} catch (Quadrige2TechnicalException e) {
491 			log.error(I18n.t("quadrige2.error.remote.upload.failed", e.getMessage()), e);
492 			throw e;
493 		} catch (IOException e) {
494 			log.error(I18n.t("quadrige2.error.remote.upload.failed", e.getMessage()), e);
495 			throw new Quadrige2TechnicalException(e);
496 		}
497 	}
498 
499 	/** {@inheritDoc} */
500 	@Override
501 	public SynchroProgressionVO startExport(AuthenticationInfo authenticationInfo,
502 			SynchroExportContextVO exportContextVO,
503 			ApplicationProgressionModel progressionModel) throws Exception {
504 		Preconditions.checkNotNull(exportContextVO);
505 
506 		// get credentials provider
507 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
508 
509 		try {
510 			// build http client
511 			try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
512 					.setDefaultCredentialsProvider(credentialsProvider)
513 					.build()) {
514 
515 				progressionModel.setMessage(t("quadrige2.synchro.progress.start"));
516 
517 				// check version compatibility
518 				checkVersion(httpClient);
519 
520 				// prepare the start request
521 				HttpPost request = createHttpPostWithStringEntity(URL_EXPORT_START, exportContextVO);
522 
523 				// execute request
524 				SynchroProgressionVO result = HttpHelper.executeRequest(httpClient, request, getGson(), SynchroProgressionVO.class);
525 
526 				progressionModel.setMessage(result.getMessage());
527 				progressionModel.setCurrent(result.getIncrement());
528 
529 				return result;
530 			}
531 		}
532 		catch(Quadrige2TechnicalException e) {
533 			throw e;
534 		}
535 		catch(Throwable e) {
536 			throw new Quadrige2TechnicalException(e);
537 		}
538 	}
539 
540 	/** {@inheritDoc} */
541 	@Override
542 	public void stopExport(AuthenticationInfo authenticationInfo,
543 			String exportJobId) throws Exception {
544 
545 		// get credentials provider
546 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
547 
548 		// build http client
549 		try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
550 				.setDefaultCredentialsProvider(credentialsProvider)
551 				.build()) {
552 
553 			HttpGet request = new HttpGet(getPathUri(String.format(URL_EXPORT_STOP, exportJobId)));
554 
555 			// execute stop request
556 			HttpHelper.executeRequest(httpClient, request);
557 
558 		}
559 	}
560 
561 	/** {@inheritDoc} */
562 	@Override
563 	public SynchroResult downloadExportResult(AuthenticationInfo authenticationInfo,
564 			SynchroExportContextVO context,
565 			ApplicationProgressionModel progressionModel) throws Exception {
566 		File exportDir = config.getTempDirectory();
567 		FileUtils.forceMkdir(exportDir);
568 		String fileName = FilenameUtils.getBaseName(context.getFileName()) + ".json";
569 		File downloadedFile = new File(exportDir, fileName);
570 
571 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
572 
573 		try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
574 				.setDefaultCredentialsProvider(credentialsProvider)
575 				.build()) {
576 
577 			progressionModel.setMessage(t("quadrige2.synchro.progress.download"));
578 
579 			// download the json file
580 			HttpGet downloadHttpGet = new HttpGet(getPathUri(String.format(URL_EXPORT_FILES, fileName)));
581 			HttpHelper.executeDownloadFileRequest(httpClient, downloadHttpGet, progressionModel, downloadedFile);
582 		}
583 
584 		SynchroResult result = null;
585 
586 		if (downloadedFile.exists()) {
587 			result = Gsons.deserializeFile(downloadedFile, SynchroResult.class);
588 			FileUtils.deleteQuietly(downloadedFile);
589 
590 			if (result == null) {
591 				throw new Quadrige2TechnicalException(t("quadrige2.error.synchro.export.resultFile.read", downloadedFile.getAbsolutePath()));
592 			}
593 		}
594 
595 		return result;
596 	}
597 
598 	/** {@inheritDoc} */
599 	@Override
600 	public void acknowledgeExport(AuthenticationInfo authenticationInfo, SynchroExportContextVO context) throws Exception {
601 		// build http client
602 		try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
603 				.setDefaultCredentialsProvider(getCredentialsProvider(authenticationInfo))
604 				.build()) {
605 
606 			// check version compatibility
607 			checkVersion(httpClient);
608 
609 			// send the acknowledge request
610 			HttpPost startHttpPost = createHttpPostWithStringEntity(URL_EXPORT_ACKNOWLEDGE, context);
611 
612 			HttpHelper.executeRequest(httpClient, startHttpPost);
613 
614 		} catch (Quadrige2TechnicalException e) {
615 			log.error(I18n.t("quadrige2.error.remote.acknowledgeExport.failed", e.getMessage()), e);
616 			// If error, be quiet ! (don't care if server could not delete its files)
617 		} catch (IOException e) {
618 			log.error(I18n.t("quadrige2.error.remote.acknowledgeExport.failed", e.getMessage()), e);
619 			throw new Quadrige2TechnicalException(e);
620 		}
621 	}
622 
623 	/** {@inheritDoc} */
624 	@Override
625 	public List<ProgramVO> savePrograms(AuthenticationInfo authenticationInfo, List<ProgramVO> programs) {
626 		return executeRemoteUpdateTyped(
627 				URL_PROGRAM_SAVE_LIST,
628 				I18n.t("quadrige2.error.remote.programs.save"),
629 				authenticationInfo,
630 				programs,
631 				new TypeToken<ArrayList<ProgramVO>>() {},
632 				true/*failed if empty response*/);
633 	}
634 
635 	/** {@inheritDoc} */
636 	@Override
637 	public List<CampaignVO> saveCampaigns(AuthenticationInfo authenticationInfo, Collection<CampaignVO> campaigns) {
638 		return executeRemoteUpdateTyped(
639 				URL_CAMPAIGN_SAVE_LIST,
640 				I18n.t("quadrige2.error.remote.campaigns.save"),
641 				authenticationInfo,
642 				campaigns,
643 				new TypeToken<ArrayList<CampaignVO>>() {},
644 				true/*failed if empty response*/);
645 	}
646 
647 	@Override
648 	public void deleteCampaigns(AuthenticationInfo authenticationInfo, Collection<Integer> campaignIds) {
649 		executeRemoteUpdate(URL_CAMPAIGN_DELETE_LIST,
650 				I18n.t("quadrige2.error.remote.campaigns.delete"),
651 				authenticationInfo,
652 				campaignIds);
653 	}
654 
655 	@Override
656 	public List<RuleListVO> saveRuleLists(AuthenticationInfo authenticationInfo, Collection<RuleListVO> ruleLists) {
657 		return executeRemoteUpdateTyped(
658 				URL_RULE_LIST_SAVE_LIST,
659 				I18n.t("quadrige2.error.remote.ruleLists.save"),
660 				authenticationInfo,
661 				ruleLists,
662 				new TypeToken<ArrayList<RuleListVO>>() {},
663 				true/*failed if empty response*/);
664 	}
665 
666 	@Override
667 	public void deleteRuleLists(AuthenticationInfo authenticationInfo, Collection<String> ruleListCds) {
668 		executeRemoteUpdate(URL_RULE_LIST_DELETE_LIST,
669 				I18n.t("quadrige2.error.remote.ruleLists.delete"),
670 				authenticationInfo,
671 				ruleListCds);
672 	}
673 
674 	@Override
675 	public SynchroProgressionVO getExportLastStatus(AuthenticationInfo authenticationInfo,
676 													String exportJobId,
677 													ApplicationProgressionModel progressionModel) throws Exception {
678 		// get credentials provider
679 		CredentialsProvider credentialsProvider = getCredentialsProvider(authenticationInfo);
680 
681 		// build http client
682 		try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
683 				.setDefaultCredentialsProvider(credentialsProvider)
684 				.build()) {
685 			return getExportLastStatus(httpClient, exportJobId, progressionModel);
686 		}
687 		catch(Quadrige2TechnicalException e) {
688 			throw e;
689 		}
690 		catch(Exception e) {
691 			throw new Quadrige2TechnicalException(e);
692 		}
693 	}
694 
695 	/* -- Internal methods -- */
696 
697 	/**
698 	 * <p>
699 	 * isRunningStatus.
700 	 * </p>
701 	 * 
702 	 * @param status
703 	 *            a {@link fr.ifremer.quadrige2.synchro.vo.SynchroProgressionStatus} object.
704 	 * @return a boolean.
705 	 */
706 	protected final boolean isRunningStatus(SynchroProgressionStatus status) {
707 		return status == SynchroProgressionStatus.RUNNING
708 				|| status == SynchroProgressionStatus.WAITING_EXECUTION;
709 	}
710 
711 	/**
712 	 * <p>
713 	 * getImportLastStatus.
714 	 * </p>
715 	 * 
716 	 * @param httpClient
717 	 *            a {@link org.apache.http.impl.client.CloseableHttpClient} object.
718 	 * @param importJobId
719 	 *            a {@link java.lang.String} object.
720 	 * @param progressionModel
721 	 *            a {@link org.nuiton.jaxx.application.type.ApplicationProgressionModel} object.
722 	 * @return a {@link fr.ifremer.quadrige2.synchro.vo.SynchroProgressionVO} object.
723 	 * @throws java.lang.Exception
724 	 *             if any.
725 	 */
726 	protected SynchroProgressionVO getImportLastStatus(CloseableHttpClient httpClient,
727 			String importJobId,
728 			ApplicationProgressionModel progressionModel) throws Exception {
729 		SynchroProgressionVO result = null;
730 		int refreshTimeout = config.getSynchronizationRefreshTimeout();
731 
732 		try {
733 			HttpGet request = new HttpGet(getPathUri(String.format(URL_IMPORT_STATUS, importJobId)));
734 
735 			progressionModel.setTotal(100);
736 
737 			SynchroProgressionStatus status = null;
738 			String filename = null;
739 			while (status == null || isRunningStatus(status)) {
740 
741 				// execute status request
742 				result = HttpHelper.executeRequest(httpClient, request, getGson(), SynchroProgressionVO.class);
743 
744 				// Save the filename (once)
745 				if (filename == null && StringUtils.isNotBlank(result.getFileName())) {
746 					filename = result.getFileName();
747 				}
748 
749 				status = SynchroProgressionStatus.valueOf(result.getStatus());
750 
751 				// update the progress model
752 				progressionModel.setMessage(result.getMessage());
753 				progressionModel.setCurrent(result.getIncrement());
754 
755 				// wait a while
756 				Thread.sleep(refreshTimeout);
757 			}
758 
759 			// Set the filename (could have disappeared in the last VO received)
760 			result.setFileName(filename);
761 
762 			return result;
763 		} catch (Quadrige2TechnicalException e) {
764 			log.error(I18n.t("quadrige2.error.remote.import.status.failed", e.getMessage()), e);
765 			throw e;
766 		}
767 	}
768 
769 	/**
770 	 * <p>
771 	 * getExportLastStatus.
772 	 * </p>
773 	 * 
774 	 * @param httpClient
775 	 *            a {@link org.apache.http.impl.client.CloseableHttpClient} object.
776 	 * @param exportJobId
777 	 *            a {@link java.lang.String} object.
778 	 * @param progressionModel
779 	 *            a {@link org.nuiton.jaxx.application.type.ApplicationProgressionModel} object.
780 	 * @return a {@link fr.ifremer.quadrige2.synchro.vo.SynchroProgressionVO} object.
781 	 * @throws java.lang.Exception
782 	 *             if any.
783 	 */
784 	protected SynchroProgressionVO getExportLastStatus(CloseableHttpClient httpClient,
785 			String exportJobId,
786 			ApplicationProgressionModel progressionModel) throws Exception {
787 		SynchroProgressionVO result = null;
788 		int refreshTimeout = config.getSynchronizationRefreshTimeout();
789 
790 		try {
791 			HttpGet request = new HttpGet(getPathUri(String.format(URL_EXPORT_STATUS, exportJobId)));
792 
793 			progressionModel.setTotal(100);
794 
795 			SynchroProgressionStatus status = null;
796 			while (status == null || isRunningStatus(status)) {
797 
798 				// execute status request
799 				result = HttpHelper.executeRequest(httpClient, request, getGson(), SynchroProgressionVO.class);
800 
801 				status = SynchroProgressionStatus.valueOf(result.getStatus());
802 
803 				// update the progress model
804 				progressionModel.setMessage(result.getMessage());
805 				progressionModel.setCurrent(result.getIncrement());
806 
807 				// wait a while
808 				Thread.sleep(refreshTimeout);
809 			}
810 
811 			return result;
812 		} catch (Quadrige2TechnicalException e) {
813 			log.error(I18n.t("quadrige2.error.remote.export.status.failed", e.getMessage()), e);
814 			throw e;
815 		}
816 	}
817 
818 	protected <R> List<R> executeRemoteUpdateTyped(
819 			final String postPath,
820 			final String errorMessagePrefix,
821 			AuthenticationInfo authenticationInfo,
822 			Object data,
823 			TypeToken<? extends List<R>> typeToken,
824 			boolean checkNotEmptyResult) {
825 
826 		List<R> result = executeRemoteUpdate(
827 				postPath,
828 				errorMessagePrefix,
829 				authenticationInfo,
830 				data,
831 				typeToken.getType()
832 				);
833 
834 		if (checkNotEmptyResult && CollectionUtils.isEmpty(result)) {
835 			log.error(errorMessagePrefix + " " + I18n.t("quadrige2.error.remote.update.emptyResponse"));
836 			throw new Quadrige2TechnicalException(errorMessagePrefix + " " + I18n.t("quadrige2.error.remote.update.emptyResponse"));
837 		}
838 
839 		return result;
840 
841 	}
842 
843 	protected void executeRemoteUpdate(
844 			final String postPath,
845 			final String logMessagePrefix,
846 			AuthenticationInfo authenticationInfo,
847 			Object data) {
848 
849 		executeRemoteUpdate(postPath, logMessagePrefix, authenticationInfo, data, null/*no result*/);
850 	}
851 
852 	protected <T> T executeRemoteUpdate(
853 			final String postPath,
854 			final String errorMessagePrefix,
855 			AuthenticationInfo authenticationInfo,
856 			Object data,
857 			Type resultType
858 			) {
859 
860 		// build http client
861 		try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(getRequestConfig())
862 				.setDefaultCredentialsProvider(getCredentialsProvider(authenticationInfo))
863 				.build()) {
864 
865 			// check version compatibility
866 			checkVersion(httpClient);
867 
868 			// create POST request
869 			HttpPost request = createHttpPostWithStringEntity(postPath, data);
870 
871 			// Execute request
872 			return HttpHelper.executeRequest(httpClient, request, getGson(), resultType);
873 
874 		} catch (BadUpdateDtException e) {
875 			throw new Quadrige2BusinessException(errorMessagePrefix + " " + I18n.t("quadrige2.error.remote.update.badUpdateDate", e.getMessage()));
876 		} catch (DataLockedException e) {
877 			throw new Quadrige2BusinessException(errorMessagePrefix + " " + I18n.t("quadrige2.error.remote.update.dataLocked"));
878 		} catch (DeleteForbiddenException e) {
879 			// this exception can be handled by client
880 			throw e;
881 		} catch (Quadrige2TechnicalException|IOException e) {
882 			log.error(errorMessagePrefix + " " + I18n.t("quadrige2.error.remote.update.failed", e.getMessage()), e);
883 			throw new Quadrige2TechnicalException(errorMessagePrefix + " " + I18n.t("quadrige2.error.remote.update.failed", e.getMessage()), e);
884 		}
885 	}
886 
887 	protected HttpPost createHttpPostWithStringEntity(
888 			final String postPath,
889 			Object data) throws DataLockedException {
890 
891 		HttpPost request = new HttpPost(getPathUri(postPath));
892 		StringEntity entity = new StringEntity(getGson().toJson(data), ContentType.APPLICATION_JSON);
893 		request.setEntity(entity);
894 
895 		return request;
896 
897 	}
898 
899 }