azkaban-aplcache
Changes
azkaban-common/build.gradle 1(+1 -0)
Details
azkaban-common/build.gradle 1(+1 -0)
diff --git a/azkaban-common/build.gradle b/azkaban-common/build.gradle
index 2b04557..ed9b316 100644
--- a/azkaban-common/build.gradle
+++ b/azkaban-common/build.gradle
@@ -34,6 +34,7 @@ model {
dependencies {
compile project(':azkaban-spi')
+ compile('com.google.inject:guice:4.1.0')
compile('com.google.guava:guava:21.0')
compile('commons-collections:commons-collections:3.2.2')
compile('org.apache.commons:commons-dbcp2:2.1.1')
diff --git a/azkaban-common/src/main/java/azkaban/AzkabanCommonModule.java b/azkaban-common/src/main/java/azkaban/AzkabanCommonModule.java
new file mode 100644
index 0000000..83aaf7c
--- /dev/null
+++ b/azkaban-common/src/main/java/azkaban/AzkabanCommonModule.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 LinkedIn Corp.
+ *
+ * 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 azkaban;
+
+import azkaban.project.JdbcProjectLoader;
+import azkaban.project.ProjectLoader;
+import azkaban.spi.Storage;
+import azkaban.spi.StorageException;
+import azkaban.storage.LocalStorage;
+import azkaban.storage.StorageConfig;
+import azkaban.storage.StorageImplementationType;
+import azkaban.utils.Props;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Scopes;
+import com.google.inject.Singleton;
+import java.io.File;
+
+import static azkaban.storage.StorageImplementationType.*;
+
+
+public class AzkabanCommonModule extends AbstractModule {
+ private final Props props;
+ /**
+ * Storage Implementation
+ * This can be any of the {@link StorageImplementationType} values in which case {@link StorageFactory} will create
+ * the appropriate storage instance. Or one can feed in a custom implementation class using the full qualified
+ * path required by a classloader.
+ *
+ * examples: LOCAL, DATABASE, azkaban.storage.MyFavStorage
+ *
+ */
+ private final String storageImplementation;
+
+ public AzkabanCommonModule(Props props) {
+ this.props = props;
+ this.storageImplementation = props.getString(Constants.ConfigurationKeys.AZKABAN_STORAGE_TYPE, LOCAL.name());
+ }
+
+ @Override
+ protected void configure() {
+ bind(ProjectLoader.class).to(JdbcProjectLoader.class).in(Scopes.SINGLETON);
+ bind(Props.class).toInstance(props);
+ bind(Storage.class).to(resolveStorageClassType()).in(Scopes.SINGLETON);
+ }
+
+ public Class<? extends Storage> resolveStorageClassType() {
+ final StorageImplementationType type = StorageImplementationType.from(storageImplementation);
+ if (type != null) {
+ return type.getImplementationClass();
+ } else {
+ return loadCustomStorageClass(storageImplementation);
+ }
+ }
+
+ private Class<? extends Storage> loadCustomStorageClass(String storageImplementation) {
+ try {
+ return (Class<? extends Storage>) Class.forName(storageImplementation);
+ } catch (ClassNotFoundException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Inject
+ public @Provides
+ LocalStorage createLocalStorage(StorageConfig config) {
+ return new LocalStorage(new File(config.getBaseDirectoryPath()));
+ }
+}
diff --git a/azkaban-common/src/main/java/azkaban/Constants.java b/azkaban-common/src/main/java/azkaban/Constants.java
index 7c7fc2e..80ab92d 100644
--- a/azkaban-common/src/main/java/azkaban/Constants.java
+++ b/azkaban-common/src/main/java/azkaban/Constants.java
@@ -84,6 +84,9 @@ public class Constants {
// Max flow running time in mins, server will kill flows running longer than this setting.
// if not set or <= 0, then there's no restriction on running time.
public static final String AZKABAN_MAX_FLOW_RUNNING_MINS = "azkaban.server.flow.max.running.minutes";
+
+ public static final String AZKABAN_STORAGE_TYPE = "azkaban.storage.type";
+ public static final String AZKABAN_STORAGE_LOCAL_BASEDIRECTORY = "azkaban.storage.local.basedirectory";
}
public static class FlowProperties {
diff --git a/azkaban-common/src/main/java/azkaban/project/JdbcProjectLoader.java b/azkaban-common/src/main/java/azkaban/project/JdbcProjectLoader.java
index 3ff410c..ced5eb0 100644
--- a/azkaban-common/src/main/java/azkaban/project/JdbcProjectLoader.java
+++ b/azkaban-common/src/main/java/azkaban/project/JdbcProjectLoader.java
@@ -16,6 +16,7 @@
package azkaban.project;
+import com.google.inject.Inject;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
@@ -62,6 +63,7 @@ public class JdbcProjectLoader extends AbstractJdbcLoader implements
private EncodingType defaultEncodingType = EncodingType.GZIP;
+ @Inject
public JdbcProjectLoader(Props props) {
super(props);
tempDir = new File(props.getString("project.temp.dir", "temp"));
diff --git a/azkaban-common/src/main/java/azkaban/ServiceProvider.java b/azkaban-common/src/main/java/azkaban/ServiceProvider.java
new file mode 100644
index 0000000..9a67e05
--- /dev/null
+++ b/azkaban-common/src/main/java/azkaban/ServiceProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 LinkedIn Corp.
+ *
+ * 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 azkaban;
+
+import com.google.inject.Injector;
+
+import static com.google.common.base.Preconditions.*;
+import static java.util.Objects.*;
+
+
+/**
+ * The {@link ServiceProvider} class is an interface to fetch any external dependency. Under the hood it simply
+ * maintains a Guice {@link Injector} which is used to fetch the required service type. The current direction of
+ * utilization of Guice is to gradually move classes into the Guice scope so that Guice can automatically resolve
+ * dependencies and provide the required services directly.
+ *
+ */
+public enum ServiceProvider {
+ SERVICE_PROVIDER;
+
+ private Injector injector = null;
+
+ /**
+ * Ensure that injector is set only once!
+ * @param injector Guice injector is itself used for providing services.
+ */
+ public synchronized void setInjector(Injector injector) {
+ checkState(this.injector == null, "Injector is already set");
+ this.injector = requireNonNull(injector, "arg injector is null");
+ }
+
+ public synchronized void unsetInjector() {
+ this.injector = null;
+ }
+
+ public <T> T getInstance(Class<T> clazz) {
+ return requireNonNull(injector).getInstance(clazz);
+ }
+
+}
diff --git a/azkaban-common/src/main/java/azkaban/storage/DatabaseStorage.java b/azkaban-common/src/main/java/azkaban/storage/DatabaseStorage.java
new file mode 100644
index 0000000..583fa78
--- /dev/null
+++ b/azkaban-common/src/main/java/azkaban/storage/DatabaseStorage.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 LinkedIn Corp.
+ *
+ * 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 azkaban.storage;
+
+import azkaban.project.JdbcProjectLoader;
+import azkaban.spi.Storage;
+import azkaban.spi.StorageMetadata;
+import java.io.InputStream;
+import java.net.URI;
+import javax.inject.Inject;
+
+
+/**
+ * DatabaseStorage
+ *
+ * This class helps in storing projects in the DB itself. This is intended to be the default since it is the current
+ * behavior of Azkaban.
+ */
+public class DatabaseStorage implements Storage {
+
+ @Inject
+ public DatabaseStorage(JdbcProjectLoader jdbcProjectLoader) {
+
+ }
+
+ @Override
+ public InputStream get(URI key) {
+ return null;
+ }
+
+ @Override
+ public URI put(StorageMetadata metadata, InputStream is) {
+ return null;
+ }
+
+ @Override
+ public boolean delete(URI key) {
+ throw new UnsupportedOperationException("Delete is not supported");
+ }
+}
diff --git a/azkaban-common/src/main/java/azkaban/storage/LocalStorage.java b/azkaban-common/src/main/java/azkaban/storage/LocalStorage.java
new file mode 100644
index 0000000..a71f131
--- /dev/null
+++ b/azkaban-common/src/main/java/azkaban/storage/LocalStorage.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 LinkedIn Corp.
+ *
+ * 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 azkaban.storage;
+
+import azkaban.spi.Storage;
+import azkaban.spi.StorageException;
+import azkaban.spi.StorageMetadata;
+import azkaban.utils.FileIOUtils;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+
+import static com.google.common.base.Preconditions.*;
+
+
+public class LocalStorage implements Storage {
+ private static final Logger log = Logger.getLogger(LocalStorage.class);
+
+ final File baseDirectory;
+
+ public LocalStorage(File baseDirectory) {
+ this.baseDirectory = validateBaseDirectory(createIfDoesNotExist(baseDirectory));
+ }
+
+ @Override
+ public InputStream get(URI key) {
+ try {
+ return new FileInputStream(getFile(key));
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ private File getFile(URI key) {
+ return new File(baseDirectory, key.getPath());
+ }
+
+ @Override
+ public URI put(StorageMetadata metadata, InputStream is) {
+
+ final File projectDir = new File(baseDirectory, metadata.getProjectId());
+ if (projectDir.mkdir()) {
+ log.info("Created project dir: " + projectDir.getAbsolutePath());
+ }
+
+ final File targetFile = new File(projectDir, metadata.getVersion() + "." + metadata.getExtension());
+
+ if (targetFile.exists()) {
+ throw new StorageException(String.format(
+ "Error in LocalStorage. Target file already exists. targetFile: %s, Metadata: %s",
+ targetFile, metadata));
+ }
+ try {
+ FileUtils.copyInputStreamToFile(is, targetFile);
+ } catch (IOException e) {
+ log.error("LocalStorage error in put(): Metadata: " + metadata);
+ throw new StorageException(e);
+ }
+ return createRelativeURI(targetFile);
+ }
+
+ private URI createRelativeURI(File targetFile) {
+ return baseDirectory.toURI().relativize(targetFile.toURI());
+ }
+
+ @Override
+ public boolean delete(URI key) {
+ throw new UnsupportedOperationException("delete has not been implemented.");
+ }
+
+ private static File createIfDoesNotExist(File baseDirectory) {
+ if(!baseDirectory.exists()) {
+ baseDirectory.mkdir();
+ log.info("Creating dir: " + baseDirectory.getAbsolutePath());
+ }
+ return baseDirectory;
+ }
+
+ private static File validateBaseDirectory(File baseDirectory) {
+ checkArgument(baseDirectory.isDirectory());
+ if (!FileIOUtils.isDirWritable(baseDirectory)) {
+ throw new IllegalArgumentException("Directory not writable: " + baseDirectory);
+ }
+ return baseDirectory;
+ }
+}
diff --git a/azkaban-common/src/main/java/azkaban/storage/StorageManager.java b/azkaban-common/src/main/java/azkaban/storage/StorageManager.java
new file mode 100644
index 0000000..6d0f714
--- /dev/null
+++ b/azkaban-common/src/main/java/azkaban/storage/StorageManager.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 LinkedIn Corp.
+ *
+ * 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 azkaban.storage;
+
+import azkaban.project.Project;
+import azkaban.spi.Storage;
+import azkaban.spi.StorageException;
+import azkaban.spi.StorageMetadata;
+import com.google.inject.Inject;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.net.URI;
+import org.apache.log4j.Logger;
+
+
+/**
+ * StorageManager manages and coordinates all interactions with the Storage layer. This also includes bookkeeping
+ * like updating DB with the new versionm, etc
+ */
+public class StorageManager {
+ private static final Logger log = Logger.getLogger(StorageManager.class);
+
+ private final Storage storage;
+
+ @Inject
+ public StorageManager(Storage storage) {
+ this.storage = storage;
+ }
+
+ /**
+ * API to a project file into Azkaban Storage
+ *
+ * @param project project ID
+ * @param fileExtension extension of the file
+ * @param filename name of the file
+ * @param localFile local file
+ * @param uploader the user who uploaded
+ */
+ public void uploadProject(
+ Project project,
+ String fileExtension,
+ String filename,
+ File localFile,
+ String uploader) {
+ final StorageMetadata metadata = new StorageMetadata(
+ String.valueOf(project.getId()),
+ String.valueOf(getLatestVersion(project)),
+ fileExtension
+ );
+ log.info(String.format(
+ "Uploading project. Uploader: %s, Metadata:%s, filename: %s[%d bytes]",
+ uploader, metadata, filename, localFile.length()
+ ));
+ try {
+ uploadProject(metadata, new FileInputStream(localFile));
+ } catch (FileNotFoundException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ private int getLatestVersion(Project project) {
+ // TODO Implement
+ return -1;
+ }
+
+ public void uploadProject(StorageMetadata metadata, InputStream is) {
+ // TODO Implement
+ URI key = storage.put(metadata, is);
+ }
+}
diff --git a/azkaban-common/src/main/java/azkaban/utils/FileIOUtils.java b/azkaban-common/src/main/java/azkaban/utils/FileIOUtils.java
index dbea348..f301b74 100644
--- a/azkaban-common/src/main/java/azkaban/utils/FileIOUtils.java
+++ b/azkaban-common/src/main/java/azkaban/utils/FileIOUtils.java
@@ -42,6 +42,31 @@ import org.apache.log4j.Logger;
public class FileIOUtils {
private final static Logger logger = Logger.getLogger(FileIOUtils.class);
+ /**
+ * Check if a directory is writable
+ *
+ * @param dir directory file object
+ * @return true if it is writable. false, otherwise
+ */
+ public static boolean isDirWritable(File dir) {
+ File testFile = null;
+ try {
+ testFile = new File(dir, "_tmp");
+ /*
+ * Create and delete a dummy file in order to check file permissions. Maybe
+ * there is a safer way for this check.
+ */
+ testFile.createNewFile();
+ } catch (IOException e) {
+ return false;
+ } finally {
+ if (testFile != null) {
+ testFile.delete();
+ }
+ }
+ return true;
+ }
+
public static class PrefixSuffixFileFilter implements FileFilter {
private String prefix;
private String suffix;
diff --git a/azkaban-common/src/test/java/azkaban/ServiceProviderTest.java b/azkaban-common/src/test/java/azkaban/ServiceProviderTest.java
new file mode 100644
index 0000000..c84ee8a
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/ServiceProviderTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 LinkedIn Corp.
+ *
+ * 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 azkaban;
+
+import azkaban.project.JdbcProjectLoader;
+import azkaban.spi.Storage;
+import azkaban.storage.DatabaseStorage;
+import azkaban.storage.LocalStorage;
+import azkaban.storage.StorageManager;
+import azkaban.utils.Props;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import java.io.File;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Test;
+
+import static azkaban.ServiceProvider.*;
+import static org.junit.Assert.*;
+
+
+public class ServiceProviderTest {
+
+ public static final String AZKABAN_LOCAL_TEST_STORAGE = "AZKABAN_LOCAL_TEST_STORAGE";
+
+ @After
+ public void tearDown() throws Exception {
+ FileUtils.deleteDirectory(new File(AZKABAN_LOCAL_TEST_STORAGE));
+ }
+
+ @Test
+ public void testInjections() throws Exception {
+ Props props = new Props();
+ props.put("database.type", "h2");
+ props.put("h2.path", "h2");
+ props.put(Constants.ConfigurationKeys.AZKABAN_STORAGE_LOCAL_BASEDIRECTORY, AZKABAN_LOCAL_TEST_STORAGE);
+
+
+ Injector injector = Guice.createInjector(
+ new AzkabanCommonModule(props)
+ );
+ SERVICE_PROVIDER.unsetInjector();
+ SERVICE_PROVIDER.setInjector(injector);
+
+ assertNotNull(SERVICE_PROVIDER.getInstance(JdbcProjectLoader.class));
+ assertNotNull(SERVICE_PROVIDER.getInstance(StorageManager.class));
+ assertNotNull(SERVICE_PROVIDER.getInstance(DatabaseStorage.class));
+ assertNotNull(SERVICE_PROVIDER.getInstance(LocalStorage.class));
+ assertNotNull(SERVICE_PROVIDER.getInstance(Storage.class));
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/storage/LocalStorageTest.java b/azkaban-common/src/test/java/azkaban/storage/LocalStorageTest.java
new file mode 100644
index 0000000..f4ff2b4
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/storage/LocalStorageTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 LinkedIn Corp.
+ *
+ * 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 azkaban.storage;
+
+import azkaban.spi.StorageMetadata;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+public class LocalStorageTest {
+ private static final Logger log = Logger.getLogger(LocalStorageTest.class);
+
+ static final String SAMPLE_FILE = "sample_flow_01.zip";
+ static final String LOCAL_STORAGE = "LOCAL_STORAGE";
+ static final File BASE_DIRECTORY = new File(LOCAL_STORAGE);
+
+ private final LocalStorage localStorage = new LocalStorage(BASE_DIRECTORY);
+
+ @Before
+ public void setUp() throws Exception {
+ tearDown();
+ BASE_DIRECTORY.mkdir();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ FileUtils.deleteDirectory(BASE_DIRECTORY);
+ }
+
+ @Test
+ public void testAll() throws Exception {
+ ClassLoader classLoader = getClass().getClassLoader();
+ File testFile = new File(classLoader.getResource(SAMPLE_FILE).getFile());
+
+ URI key;
+ try (InputStream is = new FileInputStream(testFile)) {
+ // test put
+ key = localStorage.put(new StorageMetadata("testProjectId", "1", "zip"), is);
+ }
+ assertNotNull(key);
+ log.info("Key URI: " + key);
+
+ File expectedTargetFile = new File(BASE_DIRECTORY, new StringBuilder()
+ .append("testProjectId")
+ .append(File.separator)
+ .append("1.zip")
+ .toString()
+ );
+ assertTrue(expectedTargetFile.exists());
+ assertTrue(FileUtils.contentEquals(testFile, expectedTargetFile));
+
+ // test get
+ InputStream getIs = localStorage.get(key);
+ assertNotNull(getIs);
+ File getFile = new File("tmp.get");
+ FileUtils.copyInputStreamToFile(getIs, getFile);
+ assertTrue(FileUtils.contentEquals(testFile, getFile));
+ getFile.delete();
+ }
+}
diff --git a/azkaban-common/src/test/resources/sample_flow_01.zip b/azkaban-common/src/test/resources/sample_flow_01.zip
new file mode 100644
index 0000000..1147976
Binary files /dev/null and b/azkaban-common/src/test/resources/sample_flow_01.zip differ
diff --git a/azkaban-spi/src/main/java/azkaban/spi/StorageMetadata.java b/azkaban-spi/src/main/java/azkaban/spi/StorageMetadata.java
new file mode 100644
index 0000000..9ae84c9
--- /dev/null
+++ b/azkaban-spi/src/main/java/azkaban/spi/StorageMetadata.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 LinkedIn Corp.
+ *
+ * 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 azkaban.spi;
+
+import java.util.Objects;
+
+import static java.util.Objects.*;
+
+
+public class StorageMetadata {
+ private final String projectId;
+ private final String version;
+ private final String extension;
+
+ public StorageMetadata(String projectId, String version, String extension) {
+ this.projectId = requireNonNull(projectId);
+ this.version = requireNonNull(version);
+ this.extension = requireNonNull(extension);
+ }
+
+ @Override
+ public String toString() {
+ return "StorageMetadata{" + "projectId='" + projectId + '\'' + ", version='" + version + '\'' + '}';
+ }
+
+ public String getProjectId() {
+ return projectId;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public String getExtension() {
+ return extension;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ StorageMetadata that = (StorageMetadata) o;
+ return Objects.equals(projectId, that.projectId) &&
+ Objects.equals(version, that.version) &&
+ Objects.equals(extension, that.extension);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(projectId, version, extension);
+ }
+}