/*
* 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 gobblin.runtime.api;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import com.typesafe.config.Config;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.typesafe.config.ConfigFactory;
import gobblin.annotation.Alpha;
import gobblin.configuration.ConfigurationKeys;
import gobblin.util.ConfigUtils;
import lombok.Data;
/**
* Defines a Gobblin Flow (potentially collection of {@link FlowSpec}) that can be run once, or multiple times.
* A {@link FlowSpec} is {@link Configurable} so it has an associated {@link Config}, along with
* other mandatory properties such as a uri, description, and version. A {@link FlowSpec} is
* uniquely identified by its uri (containing group and name).
*
*/
@Alpha
@Data
public class FlowSpec implements Configurable, Spec {
/** An URI identifying the flow. */
final URI uri;
/** The implementation-defined version of this spec. */
final String version;
/** Human-readable description of the flow spec */
final String description;
/** Flow config as a typesafe config object*/
final Config config;
/** Flow config as a properties collection for backwards compatibility */
// Note that this property is not strictly necessary as it can be generated from the typesafe
// config. We use it as a cache until typesafe config is more widely adopted in Gobblin.
final Properties configAsProperties;
/** URI of {@link gobblin.runtime.api.JobTemplate} to use. */
final Optional<Set<URI>> templateURIs;
/** Child {@link Spec}s to this {@link FlowSpec} **/
// Note that a FlowSpec can be materialized into multiple FlowSpec or JobSpec hierarchy
final Optional<List<Spec>> childSpecs;
public static FlowSpec.Builder builder(URI flowSpecUri) {
return new FlowSpec.Builder(flowSpecUri);
}
public static FlowSpec.Builder builder(String flowSpecUri) {
return new FlowSpec.Builder(flowSpecUri);
}
public static FlowSpec.Builder builder() {
return new FlowSpec.Builder();
}
/** Creates a builder for the FlowSpec based on values in a flow properties config. */
public static FlowSpec.Builder builder(URI catalogURI, Properties flowProps) {
String name = flowProps.getProperty(ConfigurationKeys.FLOW_NAME_KEY);
String group = flowProps.getProperty(ConfigurationKeys.FLOW_GROUP_KEY, "default");
try {
URI flowURI = new URI(catalogURI.getScheme(), catalogURI.getAuthority(),
"/" + group + "/" + name, null);
FlowSpec.Builder builder = new FlowSpec.Builder(flowURI).withConfigAsProperties(flowProps);
String descr = flowProps.getProperty(ConfigurationKeys.FLOW_DESCRIPTION_KEY, null);
if (null != descr) {
builder = builder.withDescription(descr);
}
return builder;
} catch (URISyntaxException e) {
throw new RuntimeException("Unable to create a FlowSpec URI: " + e, e);
}
}
public String toShortString() {
return getUri().toString() + "/" + getVersion();
}
public String toLongString() {
return getUri().toString() + "/" + getVersion() + "[" + getDescription() + "]";
}
@Override
public String toString() {
return toShortString();
}
/**
* Builder for {@link FlowSpec}s.
* <p> Defaults/conventions:
* <ul>
* <li> Default flowCatalogURI is {@link #DEFAULT_FLOW_CATALOG_SCHEME}:
* <li> Convention for FlowSpec URI: <flowCatalogURI>/config.get({@link ConfigurationKeys#FLOW_GROUP_KEY})/config.get({@link ConfigurationKeys#FLOW_NAME_KEY})
* <li> Convention for Description: config.get({@link ConfigurationKeys#FLOW_DESCRIPTION_KEY})
* <li> Default version: 1
* </ul>
*/
public static class Builder {
public static final String DEFAULT_FLOW_CATALOG_SCHEME = "gobblin-flow";
@VisibleForTesting
private Optional<Config> config = Optional.absent();
private Optional<Properties> configAsProperties = Optional.absent();
private Optional<URI> uri;
private String version = "1";
private Optional<String> description = Optional.absent();
private Optional<URI> flowCatalogURI = Optional.absent();
private Optional<Set<URI>> templateURIs = Optional.absent();
private Optional<List<Spec>> childSpecs = Optional.absent();
public Builder(URI flowSpecUri) {
Preconditions.checkNotNull(flowSpecUri);
this.uri = Optional.of(flowSpecUri);
}
public Builder(String flowSpecUri) {
Preconditions.checkNotNull(flowSpecUri);
Preconditions.checkNotNull(flowSpecUri);
try {
this.uri = Optional.of(new URI(flowSpecUri));
}
catch (URISyntaxException e) {
throw new RuntimeException("Invalid FlowSpec config: " + e, e);
}
}
public Builder() {
this.uri = Optional.absent();
}
public FlowSpec build() {
Preconditions.checkNotNull(this.uri);
Preconditions.checkNotNull(this.version);
return new FlowSpec(getURI(), getVersion(), getDescription(), getConfig(),
getConfigAsProperties(), getTemplateURIs(), getChildSpecs());
}
/** The scheme and authority of the flow catalog URI are used to generate FlowSpec URIs from
* flow configs. */
public FlowSpec.Builder withFlowCatalogURI(URI flowCatalogURI) {
this.flowCatalogURI = Optional.of(flowCatalogURI);
return this;
}
public FlowSpec.Builder withFlowCatalogURI(String flowCatalogURI) {
try {
this.flowCatalogURI = Optional.of(new URI(flowCatalogURI));
} catch (URISyntaxException e) {
throw new RuntimeException("Unable to set flow catalog URI: " + e, e);
}
return this;
}
public URI getDefaultFlowCatalogURI() {
try {
return new URI(DEFAULT_FLOW_CATALOG_SCHEME, null, "/", null, null);
} catch (URISyntaxException e) {
// should not happen
throw new Error("Unexpected exception: " + e, e);
}
}
public URI getFlowCatalogURI() {
if (! this.flowCatalogURI.isPresent()) {
this.flowCatalogURI = Optional.of(getDefaultFlowCatalogURI());
}
return this.flowCatalogURI.get();
}
public URI getDefaultURI() {
URI flowCatalogURI = getFlowCatalogURI();
Config flowCfg = getConfig();
String name = flowCfg.hasPath(ConfigurationKeys.FLOW_NAME_KEY) ?
flowCfg.getString(ConfigurationKeys.FLOW_NAME_KEY) :
"default";
String group = flowCfg.hasPath(ConfigurationKeys.FLOW_GROUP_KEY) ?
flowCfg.getString(ConfigurationKeys.FLOW_GROUP_KEY) :
"default";
try {
return new URI(flowCatalogURI.getScheme(), flowCatalogURI.getAuthority(),
"/" + group + "/" + name, null, null);
} catch (URISyntaxException e) {
throw new RuntimeException("Unable to create default FlowSpec URI:" + e, e);
}
}
public URI getURI() {
if (! this.uri.isPresent()) {
this.uri = Optional.of(getDefaultURI());
}
return this.uri.get();
}
public FlowSpec.Builder withVersion(String version) {
Preconditions.checkNotNull(version);
this.version = version;
return this;
}
public String getVersion() {
return this.version;
}
public FlowSpec.Builder withDescription(String flowDescription) {
Preconditions.checkNotNull(flowDescription);
this.description = Optional.of(flowDescription);
return this;
}
public String getDefaultDescription() {
Config flowConf = getConfig();
return flowConf.hasPath(ConfigurationKeys.FLOW_DESCRIPTION_KEY) ?
flowConf.getString(ConfigurationKeys.FLOW_DESCRIPTION_KEY) :
"Gobblin flow " + getURI();
}
public String getDescription() {
if (! this.description.isPresent()) {
this.description = Optional.of(getDefaultDescription());
}
return this.description.get();
}
public Config getDefaultConfig() {
return ConfigFactory.empty();
}
public Config getConfig() {
if (!this.config.isPresent()) {
this.config = this.configAsProperties.isPresent() ?
Optional.of(ConfigUtils.propertiesToTypedConfig(this.configAsProperties.get(),
Optional.<String>absent())) :
Optional.of(getDefaultConfig());
}
return this.config.get();
}
public FlowSpec.Builder withConfig(Config flowConfig) {
Preconditions.checkNotNull(flowConfig);
this.config = Optional.of(flowConfig);
return this;
}
public Properties getConfigAsProperties() {
if (!this.configAsProperties.isPresent()) {
this.configAsProperties = Optional.of(ConfigUtils.configToProperties(this.config.get()));
}
return this.configAsProperties.get();
}
public FlowSpec.Builder withConfigAsProperties(Properties flowConfig) {
Preconditions.checkNotNull(flowConfig);
this.configAsProperties = Optional.of(flowConfig);
return this;
}
public Optional<Set<URI>> getTemplateURIs() {
return this.templateURIs;
}
public FlowSpec.Builder withTemplate(URI templateURI) {
Preconditions.checkNotNull(templateURI);
if (!this.templateURIs.isPresent()) {
Set<URI> templateURISet = Sets.newHashSet();
this.templateURIs = Optional.of(templateURISet);
}
this.templateURIs.get().add(templateURI);
return this;
}
public FlowSpec.Builder withTemplates(Collection templateURIs) {
Preconditions.checkNotNull(templateURIs);
if (!this.templateURIs.isPresent()) {
Set<URI> templateURISet = Sets.newHashSet();
this.templateURIs = Optional.of(templateURISet);
}
this.templateURIs.get().addAll(templateURIs);
return this;
}
public Optional<List<Spec>> getChildSpecs() {
return this.childSpecs;
}
public FlowSpec.Builder withChildSpec(Spec childSpec) {
Preconditions.checkNotNull(childSpec);
if (!this.childSpecs.isPresent()) {
List<Spec> childSpecsList = Lists.newArrayList();
this.childSpecs = Optional.of(childSpecsList);
}
this.childSpecs.get().add(childSpec);
return this;
}
public FlowSpec.Builder withChildSpecs(List<Spec> childSpecs) {
Preconditions.checkNotNull(childSpecs);
if (!this.childSpecs.isPresent()) {
List<Spec> childSpecsList = Lists.newArrayList();
this.childSpecs = Optional.of(childSpecsList);
}
this.childSpecs.get().addAll(childSpecs);
return this;
}
}
/**
* get the private uri as the primary key for this object.
* @return
*/
public URI getUri() {
return this.uri;
}
}