/* * 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.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Properties; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import gobblin.annotation.Alpha; import gobblin.configuration.ConfigurationKeys; import gobblin.runtime.JobState; import gobblin.util.ConfigUtils; import lombok.AllArgsConstructor; import lombok.Data; /** * Defines a Gobblin Job that can be run once, or multiple times. A {@link JobSpec} is * {@link Configurable} so it has an associated {@link Config}, along with other mandatory * properties such as a uri, description, and version. A {@link JobSpec} is * uniquely identified by its uri (containing group and name). * */ @Alpha @Data @AllArgsConstructor public class JobSpec implements Configurable, Spec { private static final long serialVersionUID = 6074793380396465963L; /** An URI identifying the job. */ URI uri; /** The implementation-defined version of this spec. */ String version; /** Human-readable description of the job spec */ String description; /** Job config as a typesafe config object*/ Config config; /** Job 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. Properties configAsProperties; /** URI of {@link gobblin.runtime.api.JobTemplate} to use. */ Optional<URI> templateURI; public static Builder builder(URI jobSpecUri) { return new Builder(jobSpecUri); } public static Builder builder(String jobSpecUri) { return new Builder(jobSpecUri); } public static Builder builder() { return new Builder(); } /** Creates a builder for the JobSpec based on values in a job properties config. */ public static Builder builder(URI catalogURI, Properties jobProps) { String name = JobState.getJobNameFromProps(jobProps); String group = JobState.getJobGroupFromProps(jobProps); if (null == group) { group = "default"; } try { URI jobURI = new URI(catalogURI.getScheme(), catalogURI.getAuthority(), "/" + group + "/" + name, null); Builder builder = new Builder(jobURI).withConfigAsProperties(jobProps); String descr = JobState.getJobDescriptionFromProps(jobProps); if (null != descr) { builder.withDescription(descr); } return builder; } catch (URISyntaxException e) { throw new RuntimeException("Unable to create a JobSpec 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 JobSpec}s. * <p> Defaults/conventions: * <ul> * <li> Default jobCatalogURI is {@link #DEFAULT_JOB_CATALOG_SCHEME}: * <li> Convention for JobSpec URI: <jobCatalogURI>/config.get({@link ConfigurationKeys#JOB_GROUP_KEY})/config.get({@link ConfigurationKeys#JOB_NAME_KEY}) * <li> Convention for Description: config.get({@link ConfigurationKeys#JOB_DESCRIPTION_KEY}) * <li> Default version: 1 * </ul> */ public static class Builder { public static final String DEFAULT_JOB_CATALOG_SCHEME = "gobblin-job"; @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> jobCatalogURI = Optional.absent(); private Optional<URI> templateURI = Optional.absent(); public Builder(URI jobSpecUri) { Preconditions.checkNotNull(jobSpecUri); this.uri = Optional.of(jobSpecUri); } public Builder(String jobSpecUri) { Preconditions.checkNotNull(jobSpecUri); Preconditions.checkNotNull(jobSpecUri); try { this.uri = Optional.of(new URI(jobSpecUri)); } catch (URISyntaxException e) { throw new RuntimeException("Invalid JobSpec config: " + e, e); } } public Builder() { this.uri = Optional.absent(); } public JobSpec build() { Preconditions.checkNotNull(this.uri); Preconditions.checkNotNull(this.version); return new JobSpec(getURI(), getVersion(), getDescription(), getConfig(), getConfigAsProperties(), getTemplateURI()); } /** The scheme and authority of the job catalog URI are used to generate JobSpec URIs from * job configs. */ public Builder withJobCatalogURI(URI jobCatalogURI) { this.jobCatalogURI = Optional.of(jobCatalogURI); return this; } public Builder withJobCatalogURI(String jobCatalogURI) { try { this.jobCatalogURI = Optional.of(new URI(jobCatalogURI)); } catch (URISyntaxException e) { throw new RuntimeException("Unable to set job catalog URI: " + e, e); } return this; } public URI getDefaultJobCatalogURI() { try { return new URI(DEFAULT_JOB_CATALOG_SCHEME, null, "/", null, null); } catch (URISyntaxException e) { // should not happen throw new Error("Unexpected exception: " + e, e); } } public URI getJobCatalogURI() { if (! this.jobCatalogURI.isPresent()) { this.jobCatalogURI = Optional.of(getDefaultJobCatalogURI()); } return this.jobCatalogURI.get(); } public URI getDefaultURI() { URI jobCatalogURI = getJobCatalogURI(); Config jobCfg = getConfig(); String name = jobCfg.hasPath(ConfigurationKeys.JOB_NAME_KEY) ? jobCfg.getString(ConfigurationKeys.JOB_NAME_KEY) : "default"; String group = jobCfg.hasPath(ConfigurationKeys.JOB_GROUP_KEY) ? jobCfg.getString(ConfigurationKeys.JOB_GROUP_KEY) : "default"; try { return new URI(jobCatalogURI.getScheme(), jobCatalogURI.getAuthority(), "/" + group + "/" + name, null, null); } catch (URISyntaxException e) { throw new RuntimeException("Unable to create default JobSpec URI:" + e, e); } } public URI getURI() { if (! this.uri.isPresent()) { this.uri = Optional.of(getDefaultURI()); } return this.uri.get(); } public Builder withVersion(String version) { Preconditions.checkNotNull(version); this.version = version; return this; } public String getVersion() { return this.version; } public Builder withDescription(String jobDescription) { Preconditions.checkNotNull(jobDescription); this.description = Optional.of(jobDescription); return this; } public String getDefaultDescription() { Config jobConf = getConfig(); return jobConf.hasPath(ConfigurationKeys.JOB_DESCRIPTION_KEY) ? jobConf.getString(ConfigurationKeys.JOB_DESCRIPTION_KEY) : "Gobblin job " + 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 Builder withConfig(Config jobConfig) { Preconditions.checkNotNull(jobConfig); this.config = Optional.of(jobConfig); return this; } public Properties getConfigAsProperties() { if (!this.configAsProperties.isPresent()) { this.configAsProperties = Optional.of(ConfigUtils.configToProperties(this.config.get())); } return this.configAsProperties.get(); } public Builder withConfigAsProperties(Properties jobConfig) { Preconditions.checkNotNull(jobConfig); this.configAsProperties = Optional.of(jobConfig); return this; } public Optional<URI> getTemplateURI() { return this.templateURI; } public Builder withTemplate(URI templateURI) { Preconditions.checkNotNull(templateURI); this.templateURI = Optional.of(templateURI); return this; } } /** * get the private uri as the primary key for this object. * @return */ public URI getUri() { return this.uri; } private void writeObject(java.io.ObjectOutputStream stream) throws IOException { stream.writeObject(uri); stream.writeObject(version); stream.writeObject(description); stream.writeObject(templateURI.isPresent() ? templateURI.get() : null); stream.writeObject(configAsProperties); } private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException { uri = (URI) stream.readObject(); version = (String) stream.readObject(); description = (String) stream.readObject(); templateURI = Optional.fromNullable((URI) stream.readObject()); configAsProperties = (Properties) stream.readObject(); config = ConfigUtils.propertiesToConfig(configAsProperties); } }