• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IQSS / dataverse / #22693

03 Jul 2024 01:09PM CUT coverage: 20.626% (-0.09%) from 20.716%
#22693

push

github

web-flow
Merge pull request #10664 from IQSS/develop

merge develop into master for 6.3

195 of 1852 new or added lines in 82 files covered. (10.53%)

72 existing lines in 33 files now uncovered.

17335 of 84043 relevant lines covered (20.63%)

0.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.82
/src/main/java/edu/harvard/iq/dataverse/util/URLTokenUtil.java
1
package edu.harvard.iq.dataverse.util;
2

3
import java.util.Arrays;
4
import java.util.Random;
5
import java.util.logging.Logger;
6
import java.util.regex.Matcher;
7
import java.util.regex.Pattern;
8

9
import jakarta.json.Json;
10
import jakarta.json.JsonArray;
11
import jakarta.json.JsonArrayBuilder;
12
import jakarta.json.JsonObject;
13
import jakarta.json.JsonObjectBuilder;
14
import jakarta.json.JsonValue;
15

16
import edu.harvard.iq.dataverse.DataFile;
17
import edu.harvard.iq.dataverse.Dataset;
18
import edu.harvard.iq.dataverse.FileMetadata;
19
import edu.harvard.iq.dataverse.GlobalId;
20
import edu.harvard.iq.dataverse.authorization.users.ApiToken;
21
import edu.harvard.iq.dataverse.settings.JvmSettings;
22
import edu.harvard.iq.dataverse.util.json.JsonUtil;
23

24
import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_DRAFT;
25

26
public class URLTokenUtil {
27

28
    protected static final Logger logger = Logger.getLogger(URLTokenUtil.class.getCanonicalName());
1✔
29
    protected final DataFile dataFile;
30
    protected final Dataset dataset;
31
    protected final FileMetadata fileMetadata;
32
    protected ApiToken apiToken;
33
    protected String localeCode;
34
    
35
    
36
    public static final String HTTP_METHOD="httpMethod";
37
    public static final String TIMEOUT="timeOut";
38
    public static final String SIGNED_URL="signedUrl";
39
    public static final String NAME="name";
40
    public static final String URL_TEMPLATE="urlTemplate";
41

42
    /**
43
     * File level
44
     *
45
     * @param dataFile     Required.
46
     * @param apiToken     The apiToken can be null
47
     * @param fileMetadata Required.
48
     * @param localeCode   optional.
49
     * 
50
     */
51
    public URLTokenUtil(DataFile dataFile, ApiToken apiToken, FileMetadata fileMetadata, String localeCode)
52
            throws IllegalArgumentException {
1✔
53
        if (dataFile == null) {
1✔
54
            String error = "A DataFile is required.";
1✔
55
            logger.warning("Error in URLTokenUtil constructor: " + error);
1✔
56
            throw new IllegalArgumentException(error);
1✔
57
        }
58
        if (fileMetadata == null) {
1✔
59
            String error = "A FileMetadata is required.";
1✔
60
            logger.warning("Error in URLTokenUtil constructor: " + error);
1✔
61
            throw new IllegalArgumentException(error);
1✔
62
        }
63
        this.dataFile = dataFile;
1✔
64
        this.dataset = fileMetadata.getDatasetVersion().getDataset();
1✔
65
        this.fileMetadata = fileMetadata;
1✔
66
        this.apiToken = apiToken;
1✔
67
        this.localeCode = localeCode;
1✔
68
    }
1✔
69

70
    /**
71
     * Dataset level
72
     *
73
     * @param dataset  Required.
74
     * @param apiToken The apiToken can be null
75
     */
76
    public URLTokenUtil(Dataset dataset, ApiToken apiToken, String localeCode) {
77
        this(dataset, null, apiToken, localeCode);
1✔
78
    }
1✔
79

80
    /**
81
     * Dataset level
82
     *
83
     * @param dataset  Required.
84
     * @param datafile Optional.
85
     * @param apiToken Optional The apiToken can be null
86
     * @localeCode     Optional
87
     * 
88
     */
89
    public URLTokenUtil(Dataset dataset, DataFile datafile, ApiToken apiToken, String localeCode) {
1✔
90
        if (dataset == null) {
1✔
91
            String error = "A Dataset is required.";
×
92
            logger.warning("Error in URLTokenUtil constructor: " + error);
×
93
            throw new IllegalArgumentException(error);
×
94
        }
95
        this.dataset = dataset;
1✔
96
        this.dataFile = datafile;
1✔
97
        this.fileMetadata = null;
1✔
98
        this.apiToken = apiToken;
1✔
99
        this.localeCode = localeCode;
1✔
100
    }
1✔
101

102
    public DataFile getDataFile() {
103
        return dataFile;
1✔
104
    }
105

106
    public FileMetadata getFileMetadata() {
107
        return fileMetadata;
×
108
    }
109

110
    public ApiToken getApiToken() {
111
        return apiToken;
1✔
112
    }
113

114
    public String getLocaleCode() {
115
        return localeCode;
1✔
116
    }
117
    
118
    public JsonValue getParam(String value) {
119
        String tokenValue = null;
1✔
120
        tokenValue = getTokenValue(value);
1✔
121
        if (tokenValue != null && !tokenValue.isBlank()) {
1✔
122
            try{
123
                int x =Integer.parseInt(tokenValue);
1✔
124
                return Json.createValue(x);
1✔
125
            } catch (NumberFormatException nfe){
1✔
126
                return Json.createValue(tokenValue);
1✔
127
            }
128
        } else {
129
            return null;
1✔
130
        }
131
    }
132

133
    /**
134
     * Tries to replace all occurrences of {<text>} with the value for the
135
     * corresponding ReservedWord
136
     * 
137
     * @param url - the input string in which to replace tokens, normally a url
138
     * @throws IllegalArgumentException if there is no matching ReservedWord or if
139
     *                                  the configuation of this instance doesn't
140
     *                                  have values for this ReservedWord (e.g.
141
     *                                  asking for FILE_PID when using the dataset
142
     *                                  constructor, etc.)
143
     */
144
    public String replaceTokensWithValues(String url) {
145
        String newUrl = url;
1✔
146
        Pattern pattern = Pattern.compile("(\\{.*?\\})");
1✔
147
        Matcher matcher = pattern.matcher(url);
1✔
148
        while(matcher.find()) {
1✔
149
            String token = matcher.group(1);
1✔
150
            ReservedWord reservedWord = ReservedWord.fromString(token);
1✔
151
            String tValue = getTokenValue(token);
1✔
152
            logger.fine("Replacing " + reservedWord.toString() + " with " + tValue + " in " + newUrl);
1✔
153
            newUrl = newUrl.replace(reservedWord.toString(), tValue);
1✔
154
        }
1✔
155
        return newUrl;
1✔
156
    }
157

158
    private String getTokenValue(String value) {
159
        ReservedWord reservedWord = ReservedWord.fromString(value);
1✔
160
        switch (reservedWord) {
1✔
161
        case FILE_ID:
162
            // getDataFile is never null for file tools because of the constructor
163
            return getDataFile().getId().toString();
1✔
164
        case FILE_PID:
165
            GlobalId filePid = getDataFile().getGlobalId();
1✔
166
            if (filePid != null) {
1✔
167
                return getDataFile().getGlobalId().asString();
1✔
168
            }
169
            break;
170
        case SITE_URL:
171
            return SystemConfig.getDataverseSiteUrlStatic();
1✔
172
        case API_TOKEN:
173
            String apiTokenString = null;
1✔
174
            ApiToken theApiToken = getApiToken();
1✔
175
            if (theApiToken != null) {
1✔
176
                apiTokenString = theApiToken.getTokenString();
1✔
177
            }
178
            return apiTokenString;
1✔
179
        case DATASET_ID:
180
            return dataset.getId().toString();
1✔
181
        case DATASET_PID:
182
            return dataset.getGlobalId().asString();
1✔
183
        case DATASET_VERSION:
184
            String versionString = null;
×
185
            if (fileMetadata != null) { // true for file case
×
186
                versionString = fileMetadata.getDatasetVersion().getFriendlyVersionNumber();
×
187
            } else { // Dataset case - return the latest visible version (unless/until the dataset
188
                     // case allows specifying a version)
189
                if (getApiToken() != null) {
×
190
                    versionString = dataset.getLatestVersion().getFriendlyVersionNumber();
×
191
                } else {
192
                    versionString = dataset.getLatestVersionForCopy().getFriendlyVersionNumber();
×
193
                }
194
            }
195
            if (("DRAFT").equals(versionString)) {
×
196
                versionString = DS_VERSION_DRAFT; // send the token needed in api calls that can be substituted for a numeric version.
×
197
            }
198
            return versionString;
×
199
        case FILE_METADATA_ID:
200
            if (fileMetadata != null) { // true for file case
1✔
201
                return fileMetadata.getId().toString();
1✔
202
            }
203
        case LOCALE_CODE:
204
            return getLocaleCode();
1✔
205
        default:
206
            break;
207
        }
208
        throw new IllegalArgumentException("Cannot replace reserved word: " + value);
×
209
    }
210
    
211
    public JsonObjectBuilder createPostBody(JsonObject params, JsonArray allowedApiCalls) {
212
        JsonObjectBuilder bodyBuilder = Json.createObjectBuilder();
1✔
213
        bodyBuilder.add("queryParameters", params);
1✔
214
        if (allowedApiCalls != null && !allowedApiCalls.isEmpty()) {
1✔
215
            JsonArrayBuilder apisBuilder = Json.createArrayBuilder();
1✔
216
            allowedApiCalls.getValuesAs(JsonObject.class).forEach(((apiObj) -> {
1✔
217
                logger.fine(JsonUtil.prettyPrint(apiObj));
1✔
218
                String name = apiObj.getJsonString(NAME).getString();
1✔
219
                String httpmethod = apiObj.getJsonString(HTTP_METHOD).getString();
1✔
220
                int timeout = apiObj.getInt(TIMEOUT);
1✔
221
                String urlTemplate = apiObj.getJsonString(URL_TEMPLATE).getString();
1✔
222
                logger.fine("URL Template: " + urlTemplate);
1✔
223
                urlTemplate = SystemConfig.getDataverseSiteUrlStatic() + urlTemplate;
1✔
224
                String apiPath = replaceTokensWithValues(urlTemplate);
1✔
225
                logger.fine("URL WithTokens: " + apiPath);
1✔
226
                String url = apiPath;
1✔
227
                // Sign if apiToken exists, otherwise send unsigned URL (i.e. for guest users)
228
                ApiToken apiToken = getApiToken();
1✔
229
                if (apiToken != null) {
1✔
230
                    url = UrlSignerUtil.signUrl(apiPath, timeout, apiToken.getAuthenticatedUser().getUserIdentifier(),
1✔
231
                            httpmethod, JvmSettings.API_SIGNING_SECRET.lookupOptional().orElse("")
1✔
232
                                    + getApiToken().getTokenString());
1✔
233
                }
234
                logger.fine("Signed URL: " + url);
1✔
235
                apisBuilder.add(Json.createObjectBuilder().add(NAME, name).add(HTTP_METHOD, httpmethod)
1✔
236
                        .add(SIGNED_URL, url).add(TIMEOUT, timeout));
1✔
237
            }));
1✔
238
            bodyBuilder.add("signedUrls", apisBuilder);
1✔
239
        }
240
        return bodyBuilder;
1✔
241
    }
242

243
    public JsonObject getParams(JsonObject toolParameters) {
244
        //ToDo - why an array of object each with a single key/value pair instead of one object?
245
        JsonArray queryParams = toolParameters.getJsonArray("queryParameters");
1✔
246
    
247
        // ToDo return json and print later
248
        JsonObjectBuilder paramsBuilder = Json.createObjectBuilder();
1✔
249
        if (!(queryParams == null) && !queryParams.isEmpty()) {
1✔
250
            queryParams.getValuesAs(JsonObject.class).forEach((queryParam) -> {
1✔
251
                queryParam.keySet().forEach((key) -> {
1✔
252
                    String value = queryParam.getString(key);
1✔
253
                    JsonValue param = getParam(value);
1✔
254
                    if (param != null) {
1✔
255
                        paramsBuilder.add(key, param);
1✔
256
                    }
257
                });
1✔
258
            });
1✔
259
        }
260
        return paramsBuilder.build();
1✔
261
    }
262

263
    public static String getScriptForUrl(String url) {
264
        String msg = BundleUtil.getStringFromBundle("externaltools.enable.browser.popups");
×
NEW
265
        String newWin = "newWin" + (new Random()).nextInt(1000000000);
×
266
        //Always use a unique identifier so that more than one script can run (or one can be rerun) without conflicts
NEW
267
        String script = String.format("const %1$s = window.open('" + url + "', target='_blank'); if (!%1$s || %1$s.closed || typeof %1$s.closed == \"undefined\") {alert(\"" + msg + "\");}", newWin);
×
UNCOV
268
        return script;
×
269
   }
270

271
    public enum ReservedWord {
1✔
272

273
        // TODO: Research if a format like "{reservedWord}" is easily parse-able or if
274
        // another format would be
275
        // better. The choice of curly braces is somewhat arbitrary, but has been
276
        // observed in documentation for
277
        // various REST APIs. For example, "Variable substitutions will be made when a
278
        // variable is named in {brackets}."
279
        // from https://swagger.io/specification/#fixed-fields-29 but that's for URLs.
280
        FILE_ID("fileId"), FILE_PID("filePid"), SITE_URL("siteUrl"), API_TOKEN("apiToken"),
1✔
281
        // datasetId is the database id
282
        DATASET_ID("datasetId"),
1✔
283
        // datasetPid is the DOI or Handle
284
        DATASET_PID("datasetPid"), DATASET_VERSION("datasetVersion"), FILE_METADATA_ID("fileMetadataId"),
1✔
285
        LOCALE_CODE("localeCode");
1✔
286

287
        private final String text;
288
        private final String START = "{";
1✔
289
        private final String END = "}";
1✔
290

291
        private ReservedWord(final String text) {
1✔
292
            this.text = START + text + END;
1✔
293
        }
1✔
294

295
        /**
296
         * This is a centralized method that enforces that only reserved words are
297
         * allowed to be used by external tools. External tool authors cannot pass their
298
         * own query parameters through Dataverse such as "mode=mode1".
299
         *
300
         * @throws IllegalArgumentException
301
         */
302
        public static ReservedWord fromString(String text) throws IllegalArgumentException {
303
            if (text != null) {
1✔
304
                for (ReservedWord reservedWord : ReservedWord.values()) {
1✔
305
                    if (text.equals(reservedWord.text)) {
1✔
306
                        return reservedWord;
1✔
307
                    }
308
                }
309
            }
310
            // TODO: Consider switching to a more informative message that enumerates the
311
            // valid reserved words.
312
            boolean moreInformativeMessage = false;
1✔
313
            if (moreInformativeMessage) {
1✔
314
                throw new IllegalArgumentException(
×
315
                        "Unknown reserved word: " + text + ". A reserved word must be one of these values: "
316
                                + Arrays.asList(ReservedWord.values()) + ".");
×
317
            } else {
318
                throw new IllegalArgumentException("Unknown reserved word: " + text);
1✔
319
            }
320
        }
321

322
        @Override
323
        public String toString() {
324
            return text;
1✔
325
        }
326
    }
327
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc