1010import java .util .Arrays ;
1111import java .util .Base64 ;
1212import java .util .HashMap ;
13+ import java .util .LinkedHashSet ;
1314import java .util .List ;
1415import java .util .Map ;
16+ import java .util .Set ;
1517import java .util .concurrent .TimeUnit ;
1618import okhttp3 .Call ;
1719import okhttp3 .Callback ;
3335/** Constructor.io Client */
3436public class ConstructorIO {
3537
38+ /** Valid file extensions for catalog uploads */
39+ private static final Set <String > VALID_CATALOG_EXTENSIONS =
40+ new LinkedHashSet <>(Arrays .asList (".csv" , ".json" , ".jsonl" ));
41+
3642 /** the HTTP client used by all instances */
3743 private static OkHttpClient client =
3844 new OkHttpClient .Builder ()
@@ -1854,6 +1860,17 @@ public String recommendationsAsJSON(RecommendationsRequest req, UserInfo userInf
18541860 }
18551861 }
18561862
1863+ if (StringUtils .isNotBlank (req .getVariationId ())) {
1864+ if (req .getItemIds () == null || req .getItemIds ().size () != 1 ) {
1865+ throw new IllegalArgumentException (
1866+ "variationId requires exactly one itemId to be specified" );
1867+ }
1868+ url =
1869+ url .newBuilder ()
1870+ .addQueryParameter ("variation_id" , req .getVariationId ())
1871+ .build ();
1872+ }
1873+
18571874 if (StringUtils .isNotBlank (req .getTerm ())) {
18581875 url = url .newBuilder ().addQueryParameter ("term" , req .getTerm ()).build ();
18591876 }
@@ -2090,13 +2107,60 @@ protected static String getResponseBody(Response response) throws ConstructorExc
20902107 throw new ConstructorException (errorMessage , errorCode );
20912108 }
20922109
2110+ /**
2111+ * Validates and extracts the file extension from a File object for catalog uploads.
2112+ *
2113+ * @param file the File object containing the actual file
2114+ * @param fileName the logical file name (items, variations, item_groups)
2115+ * @return the validated file extension (including the dot, e.g., ".csv", ".json", or ".jsonl")
2116+ * @throws ConstructorException if the file extension is not in VALID_CATALOG_EXTENSIONS
2117+ */
2118+ private static String getValidatedFileExtension (File file , String fileName )
2119+ throws ConstructorException {
2120+ if (file == null ) {
2121+ throw new ConstructorException (
2122+ "Invalid file for '" + fileName + "': file cannot be null." );
2123+ }
2124+
2125+ String actualFileName = file .getName ();
2126+ if (actualFileName == null || actualFileName .isEmpty ()) {
2127+ throw new ConstructorException (
2128+ "Invalid file for '" + fileName + "': file name cannot be empty." );
2129+ }
2130+
2131+ int lastDotIndex = actualFileName .lastIndexOf ('.' );
2132+ if (lastDotIndex == -1 || lastDotIndex == actualFileName .length () - 1 ) {
2133+ throw new ConstructorException (
2134+ "Invalid file for '"
2135+ + fileName
2136+ + "': file must have "
2137+ + VALID_CATALOG_EXTENSIONS
2138+ + " extension. Found: "
2139+ + actualFileName );
2140+ }
2141+
2142+ String extension = actualFileName .substring (lastDotIndex ).toLowerCase ();
2143+
2144+ if (!VALID_CATALOG_EXTENSIONS .contains (extension )) {
2145+ throw new ConstructorException (
2146+ "Invalid file type for '"
2147+ + fileName
2148+ + "': file must have "
2149+ + VALID_CATALOG_EXTENSIONS
2150+ + " extension. Found: "
2151+ + actualFileName );
2152+ }
2153+
2154+ return extension ;
2155+ }
2156+
20932157 /**
20942158 * Grabs the version number (hard coded ATM)
20952159 *
20962160 * @return version number
20972161 */
20982162 protected String getVersion () {
2099- return "ciojava-7.2.0 " ;
2163+ return "ciojava-7.4.1 " ;
21002164 }
21012165
21022166 /**
@@ -2369,9 +2433,12 @@ protected static JSONArray transformItemsAPIV2Response(JSONArray results) {
23692433 /**
23702434 * Send a full catalog to replace the current one (sync)
23712435 *
2372- * @param req the catalog request
2373- * @return a string of JSON
2374- * @throws ConstructorException if the request is invalid.
2436+ * <p>Supports CSV, JSON, and JSONL file formats. The file type is automatically detected from
2437+ * the file extension (.csv, .json, or .jsonl).
2438+ *
2439+ * @param req the catalog request containing files with .csv, .json, or .jsonl extensions
2440+ * @return a string of JSON containing task information
2441+ * @throws ConstructorException if the request is invalid or file extensions are not supported
23752442 */
23762443 public String replaceCatalog (CatalogRequest req ) throws ConstructorException {
23772444 try {
@@ -2399,10 +2466,11 @@ public String replaceCatalog(CatalogRequest req) throws ConstructorException {
23992466 for (Map .Entry <String , File > entry : files .entrySet ()) {
24002467 String fileName = entry .getKey ();
24012468 File file = entry .getValue ();
2469+ String fileExtension = getValidatedFileExtension (file , fileName );
24022470
24032471 multipartBuilder .addFormDataPart (
24042472 fileName ,
2405- fileName + ".csv" ,
2473+ fileName + fileExtension ,
24062474 RequestBody .create (MediaType .parse ("application/octet-stream" ), file ));
24072475 }
24082476 }
@@ -2428,9 +2496,12 @@ public String replaceCatalog(CatalogRequest req) throws ConstructorException {
24282496 /**
24292497 * Send a partial catalog to update specific items (delta)
24302498 *
2431- * @param req the catalog request
2432- * @return a string of JSON
2433- * @throws ConstructorException if the request is invalid.
2499+ * <p>Supports CSV, JSON, and JSONL file formats. The file type is automatically detected from
2500+ * the file extension (.csv, .json, or .jsonl).
2501+ *
2502+ * @param req the catalog request containing files with .csv, .json, or .jsonl extensions
2503+ * @return a string of JSON containing task information
2504+ * @throws ConstructorException if the request is invalid or file extensions are not supported
24342505 */
24352506 public String updateCatalog (CatalogRequest req ) throws ConstructorException {
24362507 try {
@@ -2458,10 +2529,11 @@ public String updateCatalog(CatalogRequest req) throws ConstructorException {
24582529 for (Map .Entry <String , File > entry : files .entrySet ()) {
24592530 String fileName = entry .getKey ();
24602531 File file = entry .getValue ();
2532+ String fileExtension = getValidatedFileExtension (file , fileName );
24612533
24622534 multipartBuilder .addFormDataPart (
24632535 fileName ,
2464- fileName + ".csv" ,
2536+ fileName + fileExtension ,
24652537 RequestBody .create (MediaType .parse ("application/octet-stream" ), file ));
24662538 }
24672539 }
@@ -2488,9 +2560,12 @@ public String updateCatalog(CatalogRequest req) throws ConstructorException {
24882560 /**
24892561 * Send a patch delta catalog to update specific items (delta)
24902562 *
2491- * @param req the catalog request
2492- * @return a string of JSON
2493- * @throws ConstructorException if the request is invalid.
2563+ * <p>Supports CSV, JSON, and JSONL file formats. The file type is automatically detected from
2564+ * the file extension (.csv, .json, or .jsonl).
2565+ *
2566+ * @param req the catalog request containing files with .csv, .json, or .jsonl extensions
2567+ * @return a string of JSON containing task information
2568+ * @throws ConstructorException if the request is invalid or file extensions are not supported
24942569 */
24952570 public String patchCatalog (CatalogRequest req ) throws ConstructorException {
24962571 try {
@@ -2523,10 +2598,11 @@ public String patchCatalog(CatalogRequest req) throws ConstructorException {
25232598 for (Map .Entry <String , File > entry : files .entrySet ()) {
25242599 String fileName = entry .getKey ();
25252600 File file = entry .getValue ();
2601+ String fileExtension = getValidatedFileExtension (file , fileName );
25262602
25272603 multipartBuilder .addFormDataPart (
25282604 fileName ,
2529- fileName + ".csv" ,
2605+ fileName + fileExtension ,
25302606 RequestBody .create (MediaType .parse ("application/octet-stream" ), file ));
25312607 }
25322608 }
0 commit comments