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

temporalio / sdk-java / #175

pending completion
#175

push

github-actions

web-flow
Worker / Build Id versioning (#1786)

Implement new worker build id based versioning feature

236 of 236 new or added lines in 24 files covered. (100.0%)

18343 of 23697 relevant lines covered (77.41%)

0.81 hits per line

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

75.69
/temporal-sdk/src/main/java/io/temporal/internal/common/SearchAttributePayloadConverter.java
1
/*
2
 * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved.
3
 *
4
 * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5
 *
6
 * Modifications copyright (C) 2017 Uber Technologies, Inc.
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this material except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 *   http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20

21
package io.temporal.internal.common;
22

23
import com.google.common.base.Preconditions;
24
import com.google.common.reflect.TypeParameter;
25
import com.google.common.reflect.TypeToken;
26
import com.google.protobuf.ByteString;
27
import io.temporal.api.common.v1.Payload;
28
import io.temporal.api.enums.v1.IndexedValueType;
29
import io.temporal.common.SearchAttributeKey;
30
import io.temporal.common.SearchAttributes;
31
import io.temporal.common.converter.DataConverterException;
32
import io.temporal.common.converter.DefaultDataConverter;
33
import java.lang.reflect.Type;
34
import java.time.OffsetDateTime;
35
import java.util.*;
36
import java.util.stream.Collectors;
37
import javax.annotation.Nonnull;
38
import javax.annotation.Nullable;
39
import org.slf4j.Logger;
40
import org.slf4j.LoggerFactory;
41

42
final class SearchAttributePayloadConverter {
1✔
43
  private static final Logger log = LoggerFactory.getLogger(SearchAttributePayloadConverter.class);
1✔
44

45
  private static final String METADATA_TYPE_KEY = "type";
46

47
  public static final SearchAttributePayloadConverter INSTANCE =
1✔
48
      new SearchAttributePayloadConverter();
49

50
  public Payload encodeTyped(SearchAttributeKey<?> key, @Nullable Object value) {
51
    // We can encode as-is because we know it's strictly typed to expected key value. We
52
    // accept a null value because updates for unset can be null.
53
    return DefaultDataConverter.STANDARD_INSTANCE.toPayload(value).get().toBuilder()
1✔
54
        .putMetadata(
1✔
55
            METADATA_TYPE_KEY,
56
            ByteString.copyFromUtf8(indexValueTypeToEncodedValue(key.getValueType())))
1✔
57
        .build();
1✔
58
  }
59

60
  @SuppressWarnings({"rawtypes", "unchecked"})
61
  public void decodeTyped(SearchAttributes.Builder builder, String name, @Nonnull Payload payload) {
62
    // Get key type
63
    SearchAttributeKey key;
64
    IndexedValueType indexType = getIndexType(payload.getMetadataMap().get(METADATA_TYPE_KEY));
1✔
65
    switch (indexType) {
1✔
66
      case INDEXED_VALUE_TYPE_TEXT:
67
        key = SearchAttributeKey.forText(name);
1✔
68
        break;
1✔
69
      case INDEXED_VALUE_TYPE_KEYWORD:
70
        key = SearchAttributeKey.forKeyword(name);
1✔
71
        break;
1✔
72
      case INDEXED_VALUE_TYPE_INT:
73
        key = SearchAttributeKey.forLong(name);
1✔
74
        break;
1✔
75
      case INDEXED_VALUE_TYPE_DOUBLE:
76
        key = SearchAttributeKey.forDouble(name);
1✔
77
        break;
1✔
78
      case INDEXED_VALUE_TYPE_BOOL:
79
        key = SearchAttributeKey.forBoolean(name);
1✔
80
        break;
1✔
81
      case INDEXED_VALUE_TYPE_DATETIME:
82
        key = SearchAttributeKey.forOffsetDateTime(name);
1✔
83
        break;
1✔
84
      case INDEXED_VALUE_TYPE_KEYWORD_LIST:
85
        key = SearchAttributeKey.forKeywordList(name);
1✔
86
        break;
1✔
87
      default:
88
        log.warn(
×
89
            "[BUG] Unrecognized indexed value type {} on search attribute key {}", indexType, name);
90
        return;
×
91
    }
92

93
    // Attempt conversion to key type or if not keyword list, single-value list and extract out
94
    try {
95
      Object value =
1✔
96
          DefaultDataConverter.STANDARD_INSTANCE.fromPayload(
1✔
97
              payload, key.getValueClass(), key.getValueReflectType());
1✔
98
      // Test server can have null value, which means unset so we ignore
99
      if (value != null) {
1✔
100
        builder.set(key, value);
1✔
101
      }
102
    } catch (Exception e) {
×
103
      Exception exception = e;
×
104
      // Since it couldn't be converted using the regular type, try to convert as a single-item list
105
      // for non-keyword-list
106
      if (indexType != IndexedValueType.INDEXED_VALUE_TYPE_KEYWORD_LIST) {
×
107
        try {
108
          List items =
×
109
              DefaultDataConverter.STANDARD_INSTANCE.fromPayload(
×
110
                  payload, List.class, createListType(key.getValueClass()));
×
111
          // If it's a single-item list, we're ok and can return, but if it's not, replace outer
112
          // exception with better exception explaining situation
113
          if (items.size() == 1) {
×
114
            builder.set(key, items.get(0));
×
115
            return;
×
116
          }
117
          exception =
×
118
              new IllegalArgumentException("Unexpected list of " + items.size() + " length");
×
119
        } catch (Exception eIgnore) {
×
120
          // Just ignore the error and fall through to throw after if
121
        }
×
122
      }
123
      throw new IllegalArgumentException(
×
124
          "Search attribute " + name + " can't be deserialized", exception);
125
    }
1✔
126
  }
1✔
127

128
  /**
129
   * Convert Search Attribute object into payload with metadata. Ideally, we don't want to send the
130
   * type metadata to the server, because starting with v1.10.0 Temporal doesn't look at the type
131
   * metadata that the SDK sends. When the attribute is registered with the service, so is its
132
   * intended type which is used to validate the data. However, we do include type metadata in
133
   * payload here for compatibility with older versions of the server. Earlier version of Temporal
134
   * save the type metadata and return exactly the same payload back to the SDK, which will be
135
   * needed to deserialize the attribute into it's initial type.
136
   */
137
  public Payload encode(@Nonnull Object instance) {
138
    if (instance instanceof Collection && ((Collection<?>) instance).size() == 1) {
1✔
139
      // always serialize an empty collection as one value
140
      instance = ((Collection<?>) instance).iterator().next();
1✔
141
    }
142

143
    if (instance instanceof Payload) {
1✔
144
      // if dealing with old style search attributes and old server version we may not be able to
145
      // deserialize them
146
      // and decode will return a Payload. If it gets blindly passed further, we may end up with a
147
      // Payload here.
148
      return (Payload) instance;
×
149
    }
150

151
    Payload payload = DefaultDataConverter.STANDARD_INSTANCE.toPayload(instance).get();
1✔
152

153
    IndexedValueType type = extractIndexValueTypeName(instance);
1✔
154

155
    if (type == null) {
1✔
156
      // null returned from the previous method is a special case for UNSET
157
      return payload;
1✔
158
    }
159

160
    if (IndexedValueType.INDEXED_VALUE_TYPE_UNSPECIFIED.equals(type)) {
1✔
161
      // We can't enforce the specific types because of backwards compatibility. Previously any
162
      // value that was serializable by json (or proto json) into a string could be passed as
163
      // a search attribute
164
      // throw new IllegalArgumentException("Instance " + instance + " of class " +
165
      // instance.getClass() + " is not supported as a search attribute value");
166
      log.warn(
×
167
          "Instance {} of class {}"
168
              + " is not one of the types supported as a search attribute."
169
              + " For backwards compatibility we do the best effort to serialize it,"
170
              + " but it may cause a WorkflowTask failure after server validation.",
171
          instance,
172
          instance.getClass());
×
173
    }
174

175
    return payload.toBuilder()
1✔
176
        .putMetadata(METADATA_TYPE_KEY, ByteString.copyFromUtf8(indexValueTypeToEncodedValue(type)))
1✔
177
        .build();
1✔
178
  }
179

180
  @SuppressWarnings("deprecation")
181
  @Nonnull
182
  public List<?> decode(@Nonnull Payload payload) {
183
    ByteString dataType = payload.getMetadataMap().get(METADATA_TYPE_KEY);
1✔
184

185
    IndexedValueType indexType = getIndexType(dataType);
1✔
186
    if (isIndexTypeUndefined(indexType)) {
1✔
187
      if (isUnset(payload)) {
1✔
188
        return io.temporal.common.SearchAttribute.UNSET_VALUE;
1✔
189
      } else {
190
        log.warn("Absent or unexpected search attribute type metadata in a payload: {}", payload);
×
191
        return Collections.singletonList(payload);
×
192
      }
193
    }
194

195
    return decodeAsType(payload, indexType);
1✔
196
  }
197

198
  @Nonnull
199
  public List<?> decodeAsType(@Nonnull Payload payload, @Nonnull IndexedValueType indexType)
200
      throws DataConverterException {
201
    Preconditions.checkArgument(
1✔
202
        !isIndexTypeUndefined(indexType), "indexType can't be %s", indexType);
1✔
203

204
    ByteString data = payload.getData();
1✔
205
    if (data.isEmpty()) {
1✔
206
      log.warn("No data in payload: {}", payload);
×
207
      return Collections.singletonList(payload);
×
208
    }
209

210
    Class<?> type = indexValueTypeToJavaType(indexType);
1✔
211
    Preconditions.checkArgument(type != null);
1✔
212

213
    try {
214
      // single-value search attribute
215
      return Collections.singletonList(
1✔
216
          DefaultDataConverter.STANDARD_INSTANCE.fromPayload(payload, type, type));
1✔
217
    } catch (Exception e) {
1✔
218
      try {
219
        return DefaultDataConverter.STANDARD_INSTANCE.fromPayload(
1✔
220
            payload, List.class, createListType(type));
1✔
221
      } catch (Exception ex) {
1✔
222
        throw new IllegalArgumentException(
1✔
223
            ("Payload "
224
                + data.toStringUtf8()
1✔
225
                + " can't be deserialized into a single value or a list of "
226
                + type),
227
            ex);
228
      }
229
    }
230
  }
231

232
  private boolean isUnset(@Nonnull Payload payload) {
233
    try {
234
      List<?> o =
1✔
235
          DefaultDataConverter.STANDARD_INSTANCE.fromPayload(payload, List.class, List.class);
1✔
236
      if (o.size() == 0) {
1✔
237
        // this is an "unset" token, we don't need a type for it
238
        return true;
1✔
239
      }
240
    } catch (Exception e) {
×
241
      // ignore the exception, it was an attempt to parse a specific "unset" ('[]') value only
242
    }
×
243
    return false;
×
244
  }
245

246
  private static IndexedValueType getIndexType(ByteString dataType) {
247
    if (dataType != null) {
1✔
248
      String dataTypeString = dataType.toStringUtf8();
1✔
249
      if (dataTypeString.length() != 0) {
1✔
250
        return encodedValueToIndexValueType(dataTypeString);
1✔
251
      }
252
    }
253
    return null;
1✔
254
  }
255

256
  @Nullable
257
  private static IndexedValueType extractIndexValueTypeName(@Nonnull Object instance) {
258
    if (instance instanceof Collection) {
1✔
259
      Collection<?> collection = (Collection<?>) instance;
1✔
260
      if (!collection.isEmpty()) {
1✔
261
        List<IndexedValueType> indexValues =
1✔
262
            collection.stream()
1✔
263
                .map(k -> (javaTypeToIndexValueType(k.getClass())))
1✔
264
                .distinct()
1✔
265
                .collect(Collectors.toList());
1✔
266

267
        if (indexValues.size() == 1) {
1✔
268
          return indexValues.get(0);
1✔
269
        } else {
270
          throw new IllegalArgumentException(
×
271
              instance + " maps into a mix of IndexValueTypes: " + indexValues);
272
        }
273
      } else {
274
        // it's an "unset" value
275
        // has to be null and can't be INDEXED_VALUE_TYPE_UNSPECIFIED
276
        // because there was a bug: https://github.com/temporalio/temporal/issues/2693
277
        return null;
1✔
278
      }
279
    } else {
280
      return javaTypeToIndexValueType(instance.getClass());
1✔
281
    }
282
  }
283

284
  @Nonnull
285
  private static IndexedValueType javaTypeToIndexValueType(@Nonnull Class<?> type) {
286
    if (CharSequence.class.isAssignableFrom(type)) {
1✔
287
      return IndexedValueType.INDEXED_VALUE_TYPE_TEXT;
1✔
288
    } else if (Long.class.equals(type)
1✔
289
        || Integer.class.equals(type)
1✔
290
        || Short.class.equals(type)
1✔
291
        || Byte.class.equals(type)) {
1✔
292
      return IndexedValueType.INDEXED_VALUE_TYPE_INT;
1✔
293
    } else if (Double.class.equals(type) || Float.class.equals(type)) {
1✔
294
      return IndexedValueType.INDEXED_VALUE_TYPE_DOUBLE;
1✔
295
    } else if (Boolean.class.equals(type)) {
1✔
296
      return IndexedValueType.INDEXED_VALUE_TYPE_BOOL;
1✔
297
    } else if (OffsetDateTime.class.equals(type)) {
1✔
298
      return IndexedValueType.INDEXED_VALUE_TYPE_DATETIME;
1✔
299
    }
300
    return IndexedValueType.INDEXED_VALUE_TYPE_UNSPECIFIED;
×
301
  }
302

303
  @Nullable
304
  private static Class<?> indexValueTypeToJavaType(@Nullable IndexedValueType indexedValueType) {
305
    if (indexedValueType == null) {
1✔
306
      return null;
×
307
    }
308
    switch (indexedValueType) {
1✔
309
      case INDEXED_VALUE_TYPE_TEXT:
310
      case INDEXED_VALUE_TYPE_KEYWORD:
311
      case INDEXED_VALUE_TYPE_KEYWORD_LIST:
312
        return String.class;
1✔
313
      case INDEXED_VALUE_TYPE_INT:
314
        return Long.class;
1✔
315
      case INDEXED_VALUE_TYPE_DOUBLE:
316
        return Double.class;
1✔
317
      case INDEXED_VALUE_TYPE_BOOL:
318
        return Boolean.class;
1✔
319
      case INDEXED_VALUE_TYPE_DATETIME:
320
        return OffsetDateTime.class;
1✔
321
      case INDEXED_VALUE_TYPE_UNSPECIFIED:
322
        return null;
×
323
      default:
324
        log.warn(
×
325
            "[BUG] Mapping of IndexedValueType[{}] to Java class is not implemented",
326
            indexedValueType);
327
        return null;
×
328
    }
329
  }
330

331
  private static boolean isIndexTypeUndefined(@Nullable IndexedValueType indexType) {
332
    return indexType == null
1✔
333
        || indexType.equals(IndexedValueType.INDEXED_VALUE_TYPE_UNSPECIFIED)
1✔
334
        || indexType.equals(IndexedValueType.UNRECOGNIZED);
1✔
335
  }
336

337
  private static String indexValueTypeToEncodedValue(@Nonnull IndexedValueType indexedValueType) {
338
    return ProtoEnumNameUtils.uniqueToSimplifiedName(indexedValueType);
1✔
339
  }
340

341
  @Nullable
342
  private static IndexedValueType encodedValueToIndexValueType(String encodedValue) {
343
    try {
344
      return IndexedValueType.valueOf(
1✔
345
          ProtoEnumNameUtils.simplifiedToUniqueName(
1✔
346
              encodedValue, ProtoEnumNameUtils.INDEXED_VALUE_TYPE_PREFIX));
347
    } catch (IllegalArgumentException e) {
×
348
      log.warn("[BUG] No IndexedValueType mapping for {} value exist", encodedValue);
×
349
      return null;
×
350
    }
351
  }
352

353
  private <K> Type createListType(Class<K> elementType) {
354
    return new TypeToken<List<K>>() {}.where(new TypeParameter<K>() {}, elementType).getType();
1✔
355
  }
356
}
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