/* * Copyright © 2015 Cask Data, Inc. * * Licensed 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 co.cask.cdap.common.conf; import co.cask.cdap.api.plugin.PluginClass; import co.cask.cdap.common.InvalidArtifactException; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.artifact.ArtifactRange; import co.cask.cdap.proto.artifact.InvalidArtifactRangeException; import com.google.common.base.Charsets; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.io.Files; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import java.io.File; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; import java.util.Collections; import java.util.Map; import java.util.Set; /** * Reads files into {@link ArtifactConfig ArtifactConfigs} for a specific namespace. */ public class ArtifactConfigReader { private final LoadingCache<Id.Namespace, Gson> gsonCache; public ArtifactConfigReader() { this.gsonCache = CacheBuilder.newBuilder().build( new CacheLoader<Id.Namespace, Gson>() { @Override public Gson load(Id.Namespace namespace) throws Exception { return new GsonBuilder() .registerTypeAdapter(ArtifactRange.class, new ArtifactRangeDeserializer(namespace)) .registerTypeAdapter(ArtifactConfig.class, new ArtifactConfigDeserializer()) .registerTypeAdapter(PluginClass.class, new PluginClassDeserializer()) .create(); } }); } /** * Read the contents of the given file and deserialize it into an ArtifactConfig. * * @param namespace the namespace of the artifact this config file is for * @param configFile the config file to read * @return the contents of the file parsed as an ArtifactConfig * @throws IOException if there was a problem reading the file, for example if the file does not exist. * @throws InvalidArtifactException if there was a problem deserializing the file contents due to some invalid * format or unexpected value. */ public ArtifactConfig read(Id.Namespace namespace, File configFile) throws IOException, InvalidArtifactException { String fileName = configFile.getName(); try (Reader reader = Files.newReader(configFile, Charsets.UTF_8)) { try { ArtifactConfig config = gsonCache.getUnchecked(namespace).fromJson(reader, ArtifactConfig.class); // check namespaces in parents are either system or the specified namespace for (ArtifactRange parent : config.getParents()) { Id.Namespace parentNamespace = parent.getNamespace(); if (!parentNamespace.equals(Id.Namespace.SYSTEM) && !parent.getNamespace().equals(namespace)) { throw new InvalidArtifactException(String.format("Invalid parent %s. Parents must be in the same " + "namespace or a system artifact.", parent)); } } return config; } catch (JsonSyntaxException e) { throw new InvalidArtifactException(String.format("%s contains invalid json: %s", fileName, e.getMessage()), e); } catch (JsonParseException e) { throw new InvalidArtifactException(String.format("Unable to parse %s: %s", fileName, e.getMessage()), e); } } } /** * Deserializer for ArtifactRange in a ArtifactConfig. Artifact ranges are expected to be able to be * parsed via {@link ArtifactRange#parse(String)} or {@link ArtifactRange#parse(Id.Namespace, String)}. */ private static class ArtifactRangeDeserializer implements JsonDeserializer<ArtifactRange> { private final Id.Namespace namespace; public ArtifactRangeDeserializer(Id.Namespace namespace) { this.namespace = namespace; } @Override public ArtifactRange deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { if (!json.isJsonPrimitive()) { throw new JsonParseException("ArtifactRange should be a string."); } String rangeStr = json.getAsString(); try { if (rangeStr.indexOf(':') > 0) { return ArtifactRange.parse(rangeStr); } else { return ArtifactRange.parse(namespace, rangeStr); } } catch (InvalidArtifactRangeException e) { throw new JsonParseException(e.getMessage()); } } } /** * Deserializer for ArtifactConfig. Used to make sure collections are set to empty collections * instead of null. */ private static final class ArtifactConfigDeserializer implements JsonDeserializer<ArtifactConfig> { private static final Type PLUGINS_TYPE = new TypeToken<Set<PluginClass>>() { }.getType(); private static final Type PARENTS_TYPE = new TypeToken<Set<ArtifactRange>>() { }.getType(); private static final Type PROPERTIES_TYPE = new TypeToken<Map<String, String>>() { }.getType(); @Override public ArtifactConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { if (!json.isJsonObject()) { throw new JsonParseException("Config file must be a JSON Object."); } JsonObject obj = json.getAsJsonObject(); // deserialize fields Set<ArtifactRange> parents = context.deserialize(obj.get("parents"), PARENTS_TYPE); parents = parents == null ? Collections.<ArtifactRange>emptySet() : parents; Set<PluginClass> plugins = context.deserialize(obj.get("plugins"), PLUGINS_TYPE); plugins = plugins == null ? Collections.<PluginClass>emptySet() : plugins; Map<String, String> properties = context.deserialize(obj.get("properties"), PROPERTIES_TYPE); properties = properties == null ? Collections.<String, String>emptyMap() : properties; return new ArtifactConfig(parents, plugins, properties); } } }