/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.beam.runners.dataflow.util; import com.google.api.client.util.Data; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.apache.beam.sdk.util.StringUtils; /** * A collection of static methods for manipulating datastructure representations transferred via the * Dataflow API. */ public final class Structs { private Structs() {} // Non-instantiable public static String getString(Map<String, Object> map, String name) { return getValue(map, name, String.class, "a string"); } public static String getString( Map<String, Object> map, String name, @Nullable String defaultValue) { return getValue(map, name, String.class, "a string", defaultValue); } public static byte[] getBytes(Map<String, Object> map, String name) { @Nullable byte[] result = getBytes(map, name, null); if (result == null) { throw new ParameterNotFoundException(name, map); } return result; } @Nullable public static byte[] getBytes( Map<String, Object> map, String name, @Nullable byte[] defaultValue) { @Nullable String jsonString = getString(map, name, null); if (jsonString == null) { return defaultValue; } // TODO: Need to agree on a format for encoding bytes in // a string that can be sent to the backend, over the cloud // map task work API. base64 encoding seems pretty common. Switch to it? return StringUtils.jsonStringToByteArray(jsonString); } public static Boolean getBoolean(Map<String, Object> map, String name) { return getValue(map, name, Boolean.class, "a boolean"); } @Nullable public static Boolean getBoolean( Map<String, Object> map, String name, @Nullable Boolean defaultValue) { return getValue(map, name, Boolean.class, "a boolean", defaultValue); } public static Long getLong(Map<String, Object> map, String name) { return getValue(map, name, Long.class, "a long"); } @Nullable public static Long getLong(Map<String, Object> map, String name, @Nullable Long defaultValue) { return getValue(map, name, Long.class, "a long", defaultValue); } public static Integer getInt(Map<String, Object> map, String name) { return getValue(map, name, Integer.class, "an int"); } @Nullable public static Integer getInt( Map<String, Object> map, String name, @Nullable Integer defaultValue) { return getValue(map, name, Integer.class, "an int", defaultValue); } @Nullable public static List<String> getStrings( Map<String, Object> map, String name, @Nullable List<String> defaultValue) { @Nullable Object value = map.get(name); if (value == null) { if (map.containsKey(name)) { throw new IncorrectTypeException(name, map, "a string or a list"); } return defaultValue; } if (Data.isNull(value)) { // This is a JSON literal null. When represented as a list of strings, // this is an empty list. return Collections.<String>emptyList(); } @Nullable String singletonString = decodeValue(value, String.class); if (singletonString != null) { return Collections.singletonList(singletonString); } if (!(value instanceof List)) { throw new IncorrectTypeException(name, map, "a string or a list"); } @SuppressWarnings("unchecked") List<Object> elements = (List<Object>) value; List<String> result = new ArrayList<>(elements.size()); for (Object o : elements) { @Nullable String s = decodeValue(o, String.class); if (s == null) { throw new IncorrectTypeException(name, map, "a list of strings"); } result.add(s); } return result; } public static Map<String, Object> getObject(Map<String, Object> map, String name) { @Nullable Map<String, Object> result = getObject(map, name, null); if (result == null) { throw new ParameterNotFoundException(name, map); } return result; } @Nullable public static Map<String, Object> getObject( Map<String, Object> map, String name, @Nullable Map<String, Object> defaultValue) { @Nullable Object value = map.get(name); if (value == null) { if (map.containsKey(name)) { throw new IncorrectTypeException(name, map, "an object"); } return defaultValue; } return checkObject(value, map, name); } private static Map<String, Object> checkObject( Object value, Map<String, Object> map, String name) { if (Data.isNull(value)) { // This is a JSON literal null. When represented as an object, this is an // empty map. return Collections.<String, Object>emptyMap(); } if (!(value instanceof Map)) { throw new IncorrectTypeException(name, map, "an object (not a map)"); } @SuppressWarnings("unchecked") Map<String, Object> mapValue = (Map<String, Object>) value; if (!mapValue.containsKey(PropertyNames.OBJECT_TYPE_NAME)) { throw new IncorrectTypeException( name, map, "an object (no \"" + PropertyNames.OBJECT_TYPE_NAME + "\" field)"); } return mapValue; } @Nullable public static List<Map<String, Object>> getListOfMaps( Map<String, Object> map, String name, @Nullable List<Map<String, Object>> defaultValue) { @Nullable Object value = map.get(name); if (value == null) { if (map.containsKey(name)) { throw new IncorrectTypeException(name, map, "a list"); } return defaultValue; } if (Data.isNull(value)) { // This is a JSON literal null. When represented as a list, // this is an empty list. return Collections.<Map<String, Object>>emptyList(); } if (!(value instanceof List)) { throw new IncorrectTypeException(name, map, "a list"); } List<?> elements = (List<?>) value; for (Object elem : elements) { if (!(elem instanceof Map)) { throw new IncorrectTypeException(name, map, "a list of Map objects"); } } @SuppressWarnings("unchecked") List<Map<String, Object>> result = (List<Map<String, Object>>) elements; return result; } public static Map<String, Object> getDictionary(Map<String, Object> map, String name) { @Nullable Object value = map.get(name); if (value == null) { throw new ParameterNotFoundException(name, map); } if (Data.isNull(value)) { // This is a JSON literal null. When represented as a dictionary, this is // an empty map. return Collections.<String, Object>emptyMap(); } if (!(value instanceof Map)) { throw new IncorrectTypeException(name, map, "a dictionary"); } @SuppressWarnings("unchecked") Map<String, Object> result = (Map<String, Object>) value; return result; } @Nullable public static Map<String, Object> getDictionary( Map<String, Object> map, String name, @Nullable Map<String, Object> defaultValue) { @Nullable Object value = map.get(name); if (value == null) { if (map.containsKey(name)) { throw new IncorrectTypeException(name, map, "a dictionary"); } return defaultValue; } if (Data.isNull(value)) { // This is a JSON literal null. When represented as a dictionary, this is // an empty map. return Collections.<String, Object>emptyMap(); } if (!(value instanceof Map)) { throw new IncorrectTypeException(name, map, "a dictionary"); } @SuppressWarnings("unchecked") Map<String, Object> result = (Map<String, Object>) value; return result; } // Builder operations. public static void addString(Map<String, Object> map, String name, String value) { addObject(map, name, CloudObject.forString(value)); } public static void addBoolean(Map<String, Object> map, String name, boolean value) { addObject(map, name, CloudObject.forBoolean(value)); } public static void addLong(Map<String, Object> map, String name, long value) { addObject(map, name, CloudObject.forInteger(value)); } public static void addObject(Map<String, Object> map, String name, Map<String, Object> value) { map.put(name, value); } public static void addNull(Map<String, Object> map, String name) { map.put(name, Data.nullOf(Object.class)); } public static void addLongs(Map<String, Object> map, String name, long... longs) { List<Map<String, Object>> elements = new ArrayList<>(longs.length); for (Long value : longs) { elements.add(CloudObject.forInteger(value)); } map.put(name, elements); } public static void addList( Map<String, Object> map, String name, List<? extends Map<String, Object>> elements) { map.put(name, elements); } public static void addStringList(Map<String, Object> map, String name, List<String> elements) { ArrayList<CloudObject> objects = new ArrayList<>(elements.size()); for (String element : elements) { objects.add(CloudObject.forString(element)); } addList(map, name, objects); } public static <T extends Map<String, Object>> void addList( Map<String, Object> map, String name, T[] elements) { map.put(name, Arrays.asList(elements)); } public static void addDictionary( Map<String, Object> map, String name, Map<String, Object> value) { map.put(name, value); } public static void addDouble(Map<String, Object> map, String name, Double value) { addObject(map, name, CloudObject.forFloat(value)); } // Helper methods for a few of the accessor methods. private static <T> T getValue(Map<String, Object> map, String name, Class<T> clazz, String type) { @Nullable T result = getValue(map, name, clazz, type, null); if (result == null) { throw new ParameterNotFoundException(name, map); } return result; } @Nullable private static <T> T getValue( Map<String, Object> map, String name, Class<T> clazz, String type, @Nullable T defaultValue) { @Nullable Object value = map.get(name); if (value == null) { if (map.containsKey(name)) { throw new IncorrectTypeException(name, map, type); } return defaultValue; } T result = decodeValue(value, clazz); if (result == null) { // The value exists, but can't be decoded. throw new IncorrectTypeException(name, map, type); } return result; } @Nullable private static <T> T decodeValue(Object value, Class<T> clazz) { try { if (value.getClass() == clazz) { // decodeValue() is only called for final classes; if the class matches, // it's safe to just return the value, and if it doesn't match, decoding // is needed. return clazz.cast(value); } if (!(value instanceof Map)) { return null; } @SuppressWarnings("unchecked") Map<String, Object> map = (Map<String, Object>) value; @Nullable String typeName = (String) map.get(PropertyNames.OBJECT_TYPE_NAME); if (typeName == null) { return null; } @Nullable CloudKnownType knownType = CloudKnownType.forUri(typeName); if (knownType == null) { return null; } @Nullable Object scalar = map.get(PropertyNames.SCALAR_FIELD_NAME); if (scalar == null) { return null; } return knownType.parse(scalar, clazz); } catch (ClassCastException e) { // If any class cast fails during decoding, the value's not decodable. return null; } } private static final class ParameterNotFoundException extends RuntimeException { public ParameterNotFoundException(String name, Map<String, Object> map) { super("didn't find required parameter " + name + " in " + map); } } private static final class IncorrectTypeException extends RuntimeException { public IncorrectTypeException(String name, Map<String, Object> map, String type) { super("required parameter " + name + " in " + map + " not " + type); } } }