package com.venezuela.bcvrate.service; import java.io.BufferedReader; import java.io.InputStreamReader; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.HttpURLConnection; import java.net.URL; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; public class BCVApiService { private static final Logger log = Logger.getLogger(BCVApiService.class.getName()); private static final String BASE_URL = "https://bcv.today/api/v1"; private static final String BCV_WEBSITE = "https://www.bcv.org.ve/"; private static final int CONNECTION_TIMEOUT = 30000; private static final int READ_TIMEOUT = 60000; private static final int MAX_DAYS_BACK = 10; public BCVRateResponse getRateForDateWithFallback(String requestedDate) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); log.info("Buscando tasa para fecha solicitada: " + requestedDate); log.info("Intentando scraping BCV website..."); BCVRateResponse websiteRate = getRateFromBCVWebsite(); if (websiteRate != null && websiteRate.isValid()) { String scrapedEffectiveDate = websiteRate.getDate(); if (!scrapedEffectiveDate.equals(requestedDate)) { log.info("Scraping BCV: tasa del " + scrapedEffectiveDate + " aplica para " + requestedDate); } else { log.info("Scraping BCV: tasa del " + scrapedEffectiveDate + " coincide con fecha solicitada"); } return new BCVRateResponse(websiteRate.getDollar(), requestedDate, scrapedEffectiveDate); } log.info("Scraping BCV no disponible, buscando en API hacia atras..."); Calendar cal = Calendar.getInstance(); try { cal.setTime(sdf.parse(requestedDate)); } catch (Exception e) { log.log(Level.WARNING, "Error parseando fecha: " + requestedDate, e); return null; } for (int i = 0; i <= MAX_DAYS_BACK; i++) { String dateToQuery = sdf.format(cal.getTime()); BCVRateResponse rate = getRateFromApi(dateToQuery); if (rate != null && rate.isValid()) { log.info("Tasa encontrada (API) consultando " + dateToQuery + " para fecha solicitada " + requestedDate + ": USD=" + rate.getDollar() + " effective=" + rate.getDate()); return new BCVRateResponse(rate.getDollar(), requestedDate, rate.getDate()); } cal.add(Calendar.DAY_OF_MONTH, -1); } log.info("No se encontro tasa para: " + requestedDate); return null; } public BCVRateResponse getRateForDate(String date) { return getRateFromApi(date); } public BCVRateResponse getLatestRate() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); for (int i = 1; i <= 10; i++) { Calendar cal = Calendar.getInstance(); cal.add(Calendar.DAY_OF_MONTH, i); String futureDate = sdf.format(cal.getTime()); BCVRateResponse rate = getRateFromApi(futureDate); if (rate != null && rate.isValid()) { log.info("Tasa encontrada (API futura) para " + futureDate + ": USD=" + rate.getDollar() + " effective=" + rate.getDate()); return rate; } } log.info("API no tiene tasa futura, intentando scraping BCV..."); try { BCVRateResponse websiteRate = getRateFromBCVWebsite(); if (websiteRate != null && websiteRate.isValid()) { log.info("Tasa obtenida del sitio web BCV: USD=" + websiteRate.getDollar() + " effective=" + websiteRate.getDate()); return websiteRate; } log.info("Scraping BCV no devolvio tasa valida"); } catch (Exception e) { log.log(Level.WARNING, "Scraping BCV fallo", e); } log.info("Fallback a API actual..."); BCVRateResponse apiRate = getCurrentRate(); return apiRate; } private BCVRateResponse getRateFromApi(String date) { String urlStr = BASE_URL + "/history/" + date + ".json"; try { String response = httpGet(urlStr); if (response == null) return null; BigDecimal dollar = extractUSD(response); String effectiveDate = extractField(response, "effective_date"); if (effectiveDate == null) effectiveDate = extractField(response, "date"); if (dollar != null && effectiveDate != null) { return new BCVRateResponse(dollar, effectiveDate); } return null; } catch (Exception e) { log.log(Level.WARNING, "Error consultando fecha: " + date, e); return null; } } public BCVRateResponse getCurrentRate() { String urlStr = BASE_URL + "/rate.json"; try { String response = httpGet(urlStr); if (response == null) return null; BigDecimal dollar = extractUSD(response); String effectiveDate = extractField(response, "effective_date"); if (effectiveDate == null) effectiveDate = extractField(response, "date"); if (dollar != null && effectiveDate != null) { log.info("Tasa actual (API): USD=" + dollar + " effective=" + effectiveDate); return new BCVRateResponse(dollar, effectiveDate); } return null; } catch (Exception e) { log.log(Level.WARNING, "Error consultando tasa actual", e); return null; } } private BCVRateResponse getRateFromBCVWebsite() { int maxRetries = 3; for (int attempt = 1; attempt <= maxRetries; attempt++) { try { log.info("Scraping BCV - Intento " + attempt + "/" + maxRetries); String html = httpGetHtml(BCV_WEBSITE); if (html == null) { log.warning("Intento " + attempt + "/" + maxRetries + ": No se pudo obtener pagina BCV"); if (attempt < maxRetries) { log.info("Reintentando en 5 segundos..."); Thread.sleep(5000); continue; } log.severe("Scraping BCV fallo despues de " + maxRetries + " intentos (timeout o error de conexion)"); return null; } log.info("Pagina BCV obtenida, longitud: " + html.length()); Matcher rateMatcher = Pattern.compile( "USD.*?strong-tb\">\\s*([\\d.,]+)\\s*", Pattern.DOTALL).matcher(html); if (!rateMatcher.find()) { log.warning("No se encontro tasa USD en pagina BCV (regex no match)"); return null; } String rateStr = rateMatcher.group(1).replace(",", "."); log.info("Tasa encontrada en HTML: " + rateStr); BigDecimal dollar = new BigDecimal(rateStr).setScale(4, RoundingMode.HALF_UP); Matcher dateMatcher = Pattern.compile( "content=\"(\\d{4}-\\d{2}-\\d{2})T").matcher(html); String effectiveDate = null; if (dateMatcher.find()) { effectiveDate = dateMatcher.group(1); } if (dollar.compareTo(BigDecimal.ZERO) > 0) { if (effectiveDate == null) { effectiveDate = new SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date()); } log.info("Scraping BCV exitoso (intento " + attempt + "): USD=" + dollar + " effective=" + effectiveDate); return new BCVRateResponse(dollar, effectiveDate); } return null; } catch (InterruptedException ie) { Thread.currentThread().interrupt(); log.warning("Scraping BCV interrumpido"); return null; } catch (Exception e) { log.warning("Intento " + attempt + "/" + maxRetries + ": Error scraping BCV: " + e.getMessage()); if (attempt < maxRetries) { log.info("Reintentando en 5 segundos..."); try { Thread.sleep(5000); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } else { log.severe("Scraping BCV fallo despues de " + maxRetries + " intentos: " + e.getMessage()); } } } return null; } private BigDecimal extractUSD(String json) { Matcher m = Pattern.compile("\"USD\"\\s*:\\s*([0-9.]+)").matcher(json); if (m.find()) { return new BigDecimal(m.group(1)).setScale(4, RoundingMode.HALF_UP); } return null; } private String extractField(String json, String field) { Matcher m = Pattern.compile("\"" + field + "\"\\s*:\\s*\"([^\"]+)\"").matcher(json); if (m.find()) { return m.group(1); } return null; } private String httpGet(String urlStr) throws Exception { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Accept", "application/json"); conn.setConnectTimeout(CONNECTION_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); int responseCode = conn.getResponseCode(); if (responseCode != 200) { conn.disconnect(); return null; } StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { sb.append(line); } } finally { conn.disconnect(); } return sb.toString(); } private String httpGetHtml(String urlStr) throws Exception { URL url = new URL(urlStr); TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] certs, String authType) {} public void checkServerTrusted(X509Certificate[] certs, String authType) {} } }; SSLContext sc = SSLContext.getInstance("TLS"); sc.init(null, trustAllCerts, new SecureRandom()); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); conn.setSSLSocketFactory(sc.getSocketFactory()); conn.setHostnameVerifier((hostname, session) -> true); conn.setRequestMethod("GET"); conn.setRequestProperty("Accept", "text/html"); conn.setConnectTimeout(CONNECTION_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); int responseCode = conn.getResponseCode(); if (responseCode != 200) { conn.disconnect(); return null; } StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8"))) { String line; while ((line = reader.readLine()) != null) { sb.append(line); } } finally { conn.disconnect(); } return sb.toString(); } }