Refactor PrestashopClient and PrestashopProxyController to support raw query handling and simplify proxy GET requests

This commit is contained in:
Vincent Guillet
2025-12-02 17:35:53 +01:00
parent db8085c0aa
commit 72f3791616
2 changed files with 64 additions and 185 deletions

View File

@@ -1,15 +1,9 @@
// File: src/main/java/fr/gameovergne/api/controller/PrestashopProxyController.java
package fr.gameovergne.api.controller; package fr.gameovergne.api.controller;
import fr.gameovergne.api.service.PrestashopClient; import fr.gameovergne.api.service.PrestashopClient;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.util.AntPathMatcher; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.HandlerMapping;
@RestController @RestController
@RequestMapping("/api/ps") @RequestMapping("/api/ps")
@@ -21,37 +15,13 @@ public class PrestashopProxyController {
this.prestashopClient = prestashopClient; this.prestashopClient = prestashopClient;
} }
@GetMapping("/**") @GetMapping("/{resource}")
public ResponseEntity<String> proxyGet(HttpServletRequest request) { public ResponseEntity<String> proxyGet(
// Ex: fullPath = /api/ps/categories @PathVariable String resource,
String fullPath = (String) request.getAttribute( HttpServletRequest request
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); ) {
String bestMatchPattern = (String) request.getAttribute( String rawQuery = request.getQueryString(); // ex: "display=%5Bid,name,active%5D&output_format=JSON"
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); String body = prestashopClient.getWithRawQuery(resource, rawQuery);
return ResponseEntity.ok(body);
String relativePath = new AntPathMatcher()
.extractPathWithinPattern(bestMatchPattern, fullPath);
String path = relativePath.isEmpty() ? "/" : "/" + relativePath;
// Query string brute, déjà encodée (display=%5Bid,name,active%5D&output_format=JSON)
String rawQuery = request.getQueryString();
var prestaResponse = prestashopClient.getWithRawQuery(path, rawQuery);
// Deux options :
// 1) Propager les headers dorigine (dont Content-Type) :
/*
return ResponseEntity
.status(prestaResponse.getStatusCode())
.headers(prestaResponse.getHeaders())
.body(prestaResponse.getBody());
*/
// 2) Forcer JSON (si tu veux être sûr du Content-Type côté front) :
return ResponseEntity
.status(prestaResponse.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body(prestaResponse.getBody());
} }
} }

View File

@@ -1,179 +1,88 @@
// File: src/main/java/fr/gameovergne/api/service/PrestashopClient.java
package fr.gameovergne.api.service; package fr.gameovergne.api.service;
import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import java.net.URI;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.Map;
@Service @Service
@Slf4j
public class PrestashopClient { public class PrestashopClient {
private static final Logger log = LoggerFactory.getLogger(PrestashopClient.class);
private final RestClient client; private final RestClient client;
private final String baseUrl; private final String baseUrl;
private final String apiKey;
public PrestashopClient( public PrestashopClient(
@Value("${prestashop.base-url}") String baseUrl, @Value("${prestashop.api.base-url}") String baseUrl,
@Value("${prestashop.api-key}") String apiKey @Value("${prestashop.api.key}") String apiKey
) { ) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl; // ex: https://shop.gameovergne.fr/api
this.apiKey = apiKey;
String basicAuth = java.util.Base64.getEncoder()
.encodeToString((apiKey + ":").getBytes(StandardCharsets.UTF_8));
this.client = RestClient.builder()
.baseUrl(baseUrl)
.defaultHeaders(headers -> {
// Authentification PrestaShop via Basic Auth : key en user, mdp vide
headers.setBasicAuth(apiKey, "");
// IMPORTANT : User-Agent “neutre”
headers.set(HttpHeaders.USER_AGENT, "curl/8.10.1");
// Accept JSON
headers.setAccept(List.of(MediaType.APPLICATION_JSON, MediaType.ALL));
})
.build();
log.info("[PrestaShop] Base URL = {}", baseUrl); log.info("[PrestaShop] Base URL = {}", baseUrl);
log.info("[PrestaShop] API key length = {}", apiKey.length()); log.info("[PrestaShop] API key length = {}", apiKey.length());
this.client = RestClient.builder()
.defaultHeaders(headers -> {
// PLUS de Basic Auth => on force lauth par ws_key
headers.set(HttpHeaders.USER_AGENT, "curl/8.10.1");
headers.setAccept(List.of(MediaType.APPLICATION_JSON, MediaType.ALL));
})
.build();
} }
/** /**
* Construit lURL complète en encodant proprement les query params. * Appel "proxy" brut : on reçoit la query string telle quelle et on injecte ws_key proprement.
* Utilisé par les services "admin" (listSimple, listProducts, etc.).
*/ */
private String buildUri(String path, MultiValueMap<String, String> params) { public String getWithRawQuery(String resource, String rawQuery) {
StringBuilder sb = new StringBuilder(); try {
sb.append(baseUrl); // resource = "categories", "products", ...
StringBuilder fullUrl = new StringBuilder();
if (path != null && !path.isEmpty()) { fullUrl.append(baseUrl);
if (path.charAt(0) == '/') sb.append(path); if (!baseUrl.endsWith("/")) {
else sb.append('/').append(path); fullUrl.append("/");
} }
fullUrl.append(resource);
if (params != null && !params.isEmpty()) { // On construit la query à la main, sans que Spring y touche
boolean first = true; String encodedKey = URLEncoder.encode(apiKey, StandardCharsets.UTF_8);
for (Map.Entry<String, List<String>> entry : params.entrySet()) { fullUrl.append("?ws_key=").append(encodedKey);
String key = entry.getKey();
List<String> values = entry.getValue();
if (values == null || values.isEmpty()) continue;
for (String rawValue : values) {
if (first) {
sb.append('?');
first = false;
} else sb.append('&');
// encode la clé
sb.append(URLEncoder.encode(key, StandardCharsets.UTF_8));
sb.append('=');
// on encode seulement les parties sensibles, PAS les crochets ni virgules
String safeValue = rawValue
.replace("[", "%5B")
.replace("]", "%5D")
.replace(" ", "")
.replace(",", ","); // laisse la virgule brute
sb.append(safeValue);
}
}
}
String finalUri = sb.toString();
log.info("[PrestaShop] built URI = {}", finalUri);
return finalUri;
}
public String getJson(String path, MultiValueMap<String, String> params) {
String uri = buildUri(path, params);
log.info("[PrestaShop] GET JSON {}", uri);
return client.get()
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(String.class);
}
public String getXml(String path, MultiValueMap<String, String> params) {
String uri = buildUri(path, params);
log.info("[PrestaShop] GET XML {}", uri);
return client.get()
.uri(uri)
.accept(MediaType.APPLICATION_XML)
.retrieve()
.body(String.class);
}
public String postXml(String path, MultiValueMap<String, String> params, String xmlBody) {
String uri = buildUri(path, params);
log.info("[PrestaShop] POST XML {}", uri);
return client.post()
.uri(uri)
.contentType(MediaType.APPLICATION_XML)
.body(xmlBody)
.retrieve()
.body(String.class);
}
public String putXml(String path, MultiValueMap<String, String> params, String xmlBody) {
String uri = buildUri(path, params);
log.info("[PrestaShop] PUT XML {}", uri);
return client.put()
.uri(uri)
.contentType(MediaType.APPLICATION_XML)
.body(xmlBody)
.retrieve()
.body(String.class);
}
public void delete(String path, MultiValueMap<String, String> params) {
String uri = buildUri(path, params);
log.info("[PrestaShop] DELETE {}", uri);
client.delete()
.uri(uri)
.retrieve()
.toBodilessEntity();
}
/**
* Méthode spéciale pour le proxy brut :
* - on reçoit la query string déjà encodée (rawQuery : "display=%5Bid,name,active%5D&output_format=JSON")
* - on NE REENCODE PAS cette query
* - on renvoie un ResponseEntity<String> complet (status + headers + body)
*/
public ResponseEntity<String> getWithRawQuery(String path, String rawQuery) {
StringBuilder sb = new StringBuilder();
sb.append(baseUrl);
if (path != null && !path.isEmpty()) {
if (path.charAt(0) == '/') {
sb.append(path);
} else {
sb.append('/').append(path);
}
}
if (rawQuery != null && !rawQuery.isBlank()) { if (rawQuery != null && !rawQuery.isBlank()) {
sb.append('?').append(rawQuery); // déjà encodé // ATTENTION : rawQuery vient de request.getQueryString() donc déjà encodée.
// On lajoute telle quelle pour ne pas casser les [%5B ... %5D]
fullUrl.append("&").append(rawQuery);
} }
String uri = sb.toString(); String urlString = fullUrl.toString();
log.info("[PrestaShop] RAW GET {}", uri); log.info("[PrestaShop] RAW GET via ws_key = {}", urlString);
return client.get() return client.get()
.uri(uri) .uri(URI.create(urlString)) // IMPORTANT : on donne lURI complète => pas de ré-encodage
.accept(MediaType.APPLICATION_JSON)
.retrieve() .retrieve()
.toEntity(String.class); .body(String.class);
} catch (Exception e) {
log.error("[PrestaShop] getWithRawQuery error for resource={} rawQuery={}", resource, rawQuery, e);
throw e;
}
}
// (Optionnel) pour tes autres méthodes listSimple/listProducts,
// tu peux aussi passer par ws_key plutôt que BasicAuth, même principe :
public String getJson(String resource, String fieldsQuery) {
// ex fieldsQuery = "display=[id,name,active]&output_format=JSON"
String rawQuery = fieldsQuery; // tu peux garder le nom si tu veux
return getWithRawQuery(resource, rawQuery);
} }
} }