/**
* 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.camel.component.snakeyaml;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.DataFormatName;
import org.apache.camel.support.ServiceSupport;
import org.apache.camel.util.IOHelper;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.BaseConstructor;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.constructor.CustomClassLoaderConstructor;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;
/**
* A <a href="http://camel.apache.org/data-format.html">data format</a> ({@link DataFormat})
* using <a href="http://www.snakeyaml.org">SnakeYAML</a> to marshal to and from YAML.
*/
public final class SnakeYAMLDataFormat extends ServiceSupport implements DataFormat, DataFormatName {
private final ThreadLocal<WeakReference<Yaml>> yamlCache;
private Function<CamelContext, BaseConstructor> constructor;
private Function<CamelContext, Representer> representer;
private Function<CamelContext, DumperOptions> dumperOptions;
private Function<CamelContext, Resolver> resolver;
private ClassLoader classLoader;
private Class<?> unmarshalType;
private List<TypeDescription> typeDescriptions;
private ConcurrentMap<Class<?>, Tag> classTags;
private boolean useApplicationContextClassLoader;
private boolean prettyFlow;
private boolean allowAnyType;
private List<TypeFilter> typeFilters;
public SnakeYAMLDataFormat() {
this(null);
}
public SnakeYAMLDataFormat(Class<?> type) {
this.yamlCache = new ThreadLocal<>();
this.useApplicationContextClassLoader = true;
this.prettyFlow = false;
this.allowAnyType = false;
this.constructor = this::defaultConstructor;
this.representer = this::defaultRepresenter;
this.dumperOptions = this::defaultDumperOptions;
this.resolver = this::defaultResolver;
if (type != null) {
this.unmarshalType = type;
this.typeFilters = new CopyOnWriteArrayList<>();
this.typeFilters.add(TypeFilters.types(type));
}
}
@Override
public String getDataFormatName() {
return "yaml-snakeyaml";
}
@Override
public void marshal(final Exchange exchange, final Object graph, final OutputStream stream) throws Exception {
try (OutputStreamWriter osw = new OutputStreamWriter(stream, IOHelper.getCharsetName(exchange))) {
getYaml(exchange.getContext()).dump(graph, osw);
}
}
@Override
public Object unmarshal(final Exchange exchange, final InputStream stream) throws Exception {
try (InputStreamReader isr = new InputStreamReader(stream, IOHelper.getCharsetName(exchange))) {
Class<?> unmarshalObjectType = unmarshalType != null ? unmarshalType : Object.class;
return getYaml(exchange.getContext()).loadAs(isr, unmarshalObjectType);
}
}
@Override
protected void doStart() throws Exception {
// noop
}
@Override
protected void doStop() throws Exception {
// noop
}
protected Yaml getYaml(CamelContext context) {
Yaml yaml = null;
WeakReference<Yaml> ref = yamlCache.get();
if (ref != null) {
yaml = ref.get();
}
if (yaml == null) {
yaml = new Yaml(
this.constructor.apply(context),
this.representer.apply(context),
this.dumperOptions.apply(context),
this.resolver.apply(context)
);
yamlCache.set(new WeakReference<>(yaml));
}
return yaml;
}
public Function<CamelContext, BaseConstructor> getConstructor() {
return constructor;
}
/**
* BaseConstructor to construct incoming documents.
*/
public void setConstructor(Function<CamelContext, BaseConstructor> constructor) {
this.constructor = constructor;
}
public Function<CamelContext, Representer> getRepresenter() {
return representer;
}
/**
* Representer to emit outgoing objects.
*/
public void setRepresenter(Function<CamelContext, Representer> representer) {
this.representer = representer;
}
public Function<CamelContext, DumperOptions> getDumperOptions() {
return dumperOptions;
}
/**
* DumperOptions to configure outgoing objects.
*/
public void setDumperOptions(Function<CamelContext, DumperOptions> dumperOptions) {
this.dumperOptions = dumperOptions;
}
public Function<CamelContext, Resolver> getResolver() {
return resolver;
}
/**
* Resolver to detect implicit type
*/
public void setResolver(Function<CamelContext, Resolver> resolver) {
this.resolver = resolver;
}
public ClassLoader getClassLoader() {
return classLoader;
}
/**
* Set a custom classloader
*/
public void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
public Class<?> getUnmarshalType() {
return this.unmarshalType;
}
/**
* Class of the object to be created
*/
public void setUnmarshalType(Class<?> unmarshalType) {
this.unmarshalType = unmarshalType;
addTypeFilters(TypeFilters.types(unmarshalType));
}
public List<TypeDescription> getTypeDescriptions() {
return typeDescriptions;
}
/**
* Make YAML aware how to parse a custom Class.
*/
public void setTypeDescriptions(List<TypeDescription> typeDescriptions) {
this.typeDescriptions = new CopyOnWriteArrayList<>(typeDescriptions);
}
public void addTypeDescriptions(Collection<TypeDescription> typeDescriptions) {
if (this.typeDescriptions == null) {
this.typeDescriptions = new CopyOnWriteArrayList<>();
}
this.typeDescriptions.addAll(typeDescriptions);
}
public void addTypeDescriptions(TypeDescription... typeDescriptions) {
addTypeDescriptions(Arrays.asList(typeDescriptions));
}
public void addTypeDescription(Class<?> type, Tag tag) {
if (this.typeDescriptions == null) {
this.typeDescriptions = new CopyOnWriteArrayList<>();
}
this.typeDescriptions.add(new TypeDescription(type, tag));
}
public Map<Class<?>, Tag> getClassTags() {
return classTags;
}
/**
* Define a tag for the <code>Class</code> to serialize.
*/
public void setClassTags(Map<Class<?>, Tag> classTags) {
this.classTags = new ConcurrentHashMap<>();
this.classTags.putAll(classTags);
}
public void addClassTags(Class<?> type, Tag tag) {
if (this.classTags == null) {
this.classTags = new ConcurrentHashMap<>();
}
this.classTags.put(type, tag);
}
public boolean isUseApplicationContextClassLoader() {
return useApplicationContextClassLoader;
}
/**
* Use ApplicationContextClassLoader as custom ClassLoader
*/
public void setUseApplicationContextClassLoader(boolean useApplicationContextClassLoader) {
this.useApplicationContextClassLoader = useApplicationContextClassLoader;
}
public boolean isPrettyFlow() {
return prettyFlow;
}
/**
* Force the emitter to produce a pretty YAML document when using the flow
* style.
*/
public void setPrettyFlow(boolean prettyFlow) {
this.prettyFlow = prettyFlow;
}
/**
* Convenience method to set class tag for bot <code>Constructor</code> and
* <code>Representer</code>
*/
public void addTag(Class<?> type, Tag tag) {
addClassTags(type, tag);
addTypeDescription(type, tag);
}
public List<TypeFilter> getTypeFilters() {
return typeFilters;
}
/**
* Set the types SnakeYAML is allowed to un-marshall
*/
public void setTypeFilters(List<TypeFilter> typeFilters) {
this.typeFilters = new CopyOnWriteArrayList<>(typeFilters);
}
public void setTypeFilterDefinitions(List<String> typeFilterDefinitions) {
this.typeFilters = new CopyOnWriteArrayList<>();
for (String definition : typeFilterDefinitions) {
TypeFilters.valueOf(definition).ifPresent(this.typeFilters::add);
}
}
public void addTypeFilters(Collection<TypeFilter> typeFilters) {
if (this.typeFilters == null) {
this.typeFilters = new CopyOnWriteArrayList<>();
}
this.typeFilters.addAll(typeFilters);
}
public void addTypeFilters(TypeFilter... typeFilters) {
addTypeFilters(Arrays.asList(typeFilters));
}
public boolean isAllowAnyType() {
return allowAnyType;
}
/**
* Allow any class to be un-marshaled, same as setTypeFilters(TypeFilters.allowAll())
*/
public void setAllowAnyType(boolean allowAnyType) {
this.allowAnyType = allowAnyType;
}
// ***************************
// Defaults
// ***************************
private BaseConstructor defaultConstructor(CamelContext context) {
ClassLoader yamlClassLoader = this.classLoader;
Collection<TypeFilter> yamlTypeFilters = this.typeFilters;
if (yamlClassLoader == null && useApplicationContextClassLoader) {
yamlClassLoader = context.getApplicationContextClassLoader();
}
if (allowAnyType) {
yamlTypeFilters = Collections.singletonList(TypeFilters.allowAll());
}
BaseConstructor yamlConstructor;
if (yamlTypeFilters != null) {
yamlConstructor = yamlClassLoader != null
? typeFilterConstructor(yamlClassLoader, yamlTypeFilters)
: typeFilterConstructor(yamlTypeFilters);
} else {
yamlConstructor = new SafeConstructor();
}
if (typeDescriptions != null && yamlConstructor instanceof Constructor) {
for (TypeDescription typeDescription : typeDescriptions) {
((Constructor)yamlConstructor).addTypeDescription(typeDescription);
}
}
return yamlConstructor;
}
private Representer defaultRepresenter(CamelContext context) {
Representer yamlRepresenter = new Representer();
if (classTags != null) {
for (Map.Entry<Class<?>, Tag> entry : classTags.entrySet()) {
yamlRepresenter.addClassTag(entry.getKey(), entry.getValue());
}
}
return yamlRepresenter;
}
private DumperOptions defaultDumperOptions(CamelContext context) {
DumperOptions yamlDumperOptions = new DumperOptions();
yamlDumperOptions.setPrettyFlow(prettyFlow);
return yamlDumperOptions;
}
private Resolver defaultResolver(CamelContext context) {
return new Resolver();
}
// ***************************
// Constructors
// ***************************
private static Constructor typeFilterConstructor(final Collection<TypeFilter> typeFilters) {
return new Constructor() {
@Override
protected Class<?> getClassForName(String name) throws ClassNotFoundException {
if (typeFilters.stream().noneMatch(f -> f.test(name))) {
throw new IllegalArgumentException("Type " + name + " is not allowed");
}
return super.getClassForName(name);
}
};
}
private static Constructor typeFilterConstructor(final ClassLoader classLoader, final Collection<TypeFilter> typeFilters) {
return new CustomClassLoaderConstructor(classLoader) {
@Override
protected Class<?> getClassForName(String name) throws ClassNotFoundException {
if (typeFilters.stream().noneMatch(f -> f.test(name))) {
throw new IllegalArgumentException("Type " + name + " is not allowed");
}
return super.getClassForName(name);
}
};
}
}