/**
* Copyright 2011-2017 Asakusa Framework Team.
*
* 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 com.asakusafw.yaess.multidispatch;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.asakusafw.yaess.core.ExecutionContext;
import com.asakusafw.yaess.core.ExecutionMonitor;
import com.asakusafw.yaess.core.ExecutionScript;
import com.asakusafw.yaess.core.ExecutionScriptHandler;
import com.asakusafw.yaess.core.ServiceProfile;
import com.asakusafw.yaess.core.YaessLogger;
import com.asakusafw.yaess.core.util.PropertiesUtil;
/**
* A Dispatcher implementation for {@link ExecutionScriptHandler}.
* @param <T> the target script kind
* @since 0.2.6
*/
public abstract class ExecutionScriptHandlerDispatcher<T extends ExecutionScript>
implements ExecutionScriptHandler<T> {
static final YaessLogger YSLOG = new YaessMultiDispatchLogger(ExecutionScriptHandlerDispatcher.class);
static final Logger LOG = LoggerFactory.getLogger(ExecutionScriptHandlerDispatcher.class);
private static final String LABEL_UNDEFINED = "(undefined)";
static final String PREFIX_CONF = "conf";
static final String KEY_DIRECTORY = PREFIX_CONF + ".directory";
static final String KEY_SETUP = PREFIX_CONF + ".setup";
static final String KEY_CLEANUP = PREFIX_CONF + ".cleanup";
static final String PREFIX_DEFAULT = "default";
static final String SUFFIX_CONF = ".properties";
private final Class<? extends ExecutionScriptHandler<T>> handlerKind;
private volatile String prefix;
private volatile File confDirectory;
private volatile Reference<Map<String, Properties>> confCache = new SoftReference<>(null);
private volatile Map<String, ExecutionScriptHandler<T>> delegations;
private volatile String forceSetUp;
private volatile String forceCleanUp;
/**
* Creates a new instance.
* @param handlerKind the handler kind
* @throws IllegalArgumentException if some parameters were {@code null}
*/
protected ExecutionScriptHandlerDispatcher(Class<? extends ExecutionScriptHandler<T>> handlerKind) {
if (handlerKind == null) {
throw new IllegalArgumentException("handlerKind must not be null"); //$NON-NLS-1$
}
this.handlerKind = handlerKind;
}
@Override
public void configure(ServiceProfile<?> profile) throws IOException, InterruptedException {
this.prefix = profile.getPrefix();
try {
this.confDirectory = getConfDirectory(profile);
this.delegations = getDelegations(profile);
this.forceSetUp = profile.getConfiguration(KEY_SETUP, false, true);
this.forceCleanUp = profile.getConfiguration(KEY_CLEANUP, false, true);
} catch (IllegalArgumentException e) {
throw new IOException(MessageFormat.format(
"Failed to configure \"{0}\" ({1})",
profile.getPrefix(),
profile.getServiceClass().getName()), e);
}
if (forceSetUp != null && delegations.containsKey(forceSetUp) == false) {
throw new IOException(MessageFormat.format(
"Failed to detect setUp target: \"{2}\" in {0}.{1}",
profile.getPrefix(),
KEY_SETUP,
forceSetUp));
}
if (forceCleanUp != null && delegations.containsKey(forceCleanUp) == false) {
throw new IOException(MessageFormat.format(
"Failed to detect cleanUp target: \"{2}\" in {0}.{1}",
profile.getPrefix(),
KEY_CLEANUP,
forceCleanUp));
}
}
private File getConfDirectory(ServiceProfile<?> profile) {
assert profile != null;
String value = profile.getConfiguration(KEY_DIRECTORY, true, true);
File dir = new File(value);
if (dir.exists() == false) {
YSLOG.info("I00001",
profile.getPrefix(),
KEY_DIRECTORY,
value);
}
return dir;
}
private Map<String, ExecutionScriptHandler<T>> getDelegations(
ServiceProfile<?> profile) throws IOException, InterruptedException {
assert profile != null;
Map<String, String> conf = profile.getConfiguration();
Set<String> keys = PropertiesUtil.getChildKeys(conf, "", ".");
keys.remove(PREFIX_CONF);
if (keys.contains(PREFIX_DEFAULT) == false) {
throw new IOException(MessageFormat.format(
"Default profile for multidispatch plugin is not defined: {0}.{1}",
profile.getPrefix(),
PREFIX_DEFAULT));
}
Properties properties = new Properties();
for (Map.Entry<String, String> entry : conf.entrySet()) {
String key = profile.getPrefix() + "." + entry.getKey();
String value = entry.getValue();
properties.setProperty(key, value);
}
Map<String, ExecutionScriptHandler<T>> results = new HashMap<>();
for (String key : keys) {
String subPrefix = profile.getPrefix() + "." + key;
ServiceProfile<? extends ExecutionScriptHandler<T>> subProfile;
try {
subProfile = ServiceProfile.load(
properties,
subPrefix,
handlerKind,
profile.getContext());
} catch (IllegalArgumentException e) {
throw new IOException(MessageFormat.format(
"Failed to load sub component for multidispatch plugin: {0}",
subPrefix), e);
}
ExecutionScriptHandler<T> subInstance = subProfile.newInstance();
results.put(key, subInstance);
}
return results;
}
@Override
public String getHandlerId() {
return prefix;
}
private ExecutionScriptHandler<T> resolve(
ExecutionContext context,
ExecutionScript script) throws IOException {
assert context != null;
Properties batchConf = getBatchConf(context, script);
String key = findKey(context, script, batchConf);
if (key != null) {
ExecutionScriptHandler<T> target = delegations.get(key);
if (target != null) {
return target;
}
throw new IOException(MessageFormat.format(
"Invalid dispatch target for multidispatch plugin: "
+ "{4} (batchId={0}, flowId={1}, phase={2}, stageId={3})",
context.getBatchId(),
context.getFlowId(),
context.getPhase(),
script == null ? LABEL_UNDEFINED : script.getId(),
key));
}
ExecutionScriptHandler<T> defaultTarget = delegations.get(PREFIX_DEFAULT);
assert defaultTarget != null;
return defaultTarget;
}
private String findKey(ExecutionContext context, ExecutionScript script, Properties batchConf) {
if (batchConf != null) {
for (FindPattern pattern : FindPattern.values()) {
String key = pattern.getKey(context, script);
if (key == null) {
continue;
}
String value = batchConf.getProperty(key);
if (value == null) {
continue;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Dispatch target found (batchId={}, flowId={}, phase={}, stageId={}, key={}, value={})",
new Object[] {
context.getBatchId(),
context.getFlowId(),
context.getPhase(),
script == null ? LABEL_UNDEFINED : script.getId(),
key,
value,
}
);
}
return value;
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Dispatch target does not found (batchId={}, flowId={}, phase={}, stageId={})", new Object[] {
context.getBatchId(),
context.getFlowId(),
context.getPhase().getSymbol(),
script == null ? LABEL_UNDEFINED : script.getId(),
});
}
return null;
}
private Properties getBatchConf(ExecutionContext context, ExecutionScript script) throws IOException {
assert context != null;
Map<String, Properties> cached = confCache.get();
if (cached != null) {
String batchId = context.getBatchId();
synchronized (this) {
if (cached.containsKey(batchId)) {
return cached.get(batchId);
}
}
}
Properties batchConf = loadBatchConf(context, script);
synchronized (this) {
cached = confCache.get();
if (cached == null) {
cached = new HashMap<>();
confCache = new SoftReference<>(cached);
}
cached.put(context.getBatchId(), batchConf);
}
return batchConf;
}
private Properties loadBatchConf(ExecutionContext context, ExecutionScript script) throws IOException {
assert context != null;
String fileName = context.getBatchId() + SUFFIX_CONF;
File file = new File(confDirectory, fileName);
LOG.debug("Finding multidispatch configuration file: batchId={}, file={}", context.getBatchId(), file);
if (file.isFile() == false) {
LOG.debug("Missing multidispatch configuration file: batchId={}, file={}", context.getBatchId(), file);
return null;
}
LOG.debug("Loading multidispatch configuration file: batchId={}, file={}", context.getBatchId(), file);
try (InputStream in = new FileInputStream(file)) {
Properties properties = new Properties();
properties.load(in);
return properties;
} catch (IOException e) {
YSLOG.error(e, "E01001",
context.getBatchId(),
file.getAbsolutePath());
throw e;
}
}
@Override
public String getResourceId(
ExecutionContext context,
ExecutionScript script) throws InterruptedException, IOException {
ExecutionScriptHandler<T> target = resolve(context, script);
return target.getResourceId(context, script);
}
@Override
public Map<String, String> getProperties(
ExecutionContext context,
ExecutionScript script) throws InterruptedException, IOException {
ExecutionScriptHandler<T> target = resolve(context, script);
return target.getProperties(context, script);
}
@Override
public Map<String, String> getEnvironmentVariables(
ExecutionContext context,
ExecutionScript script) throws InterruptedException, IOException {
ExecutionScriptHandler<T> target = resolve(context, script);
return target.getEnvironmentVariables(context, script);
}
@Override
public void setUp(ExecutionMonitor monitor, ExecutionContext context) throws InterruptedException, IOException {
ExecutionScriptHandler<T> target;
if (forceSetUp != null) {
target = delegations.get(forceSetUp);
} else {
target = resolve(context, null);
}
assert target != null;
YSLOG.info("I01001",
target.getHandlerId(),
context.getBatchId(),
context.getFlowId(),
context.getPhase(),
context.getExecutionId());
target.setUp(monitor, context);
}
@Override
public void execute(
ExecutionMonitor monitor,
ExecutionContext context,
T script) throws InterruptedException, IOException {
ExecutionScriptHandler<T> target = resolve(context, script);
assert target != null;
YSLOG.info("I01002",
target.getHandlerId(),
context.getBatchId(),
context.getFlowId(),
context.getPhase(),
context.getExecutionId(),
script.getId());
target.execute(monitor, context, script);
}
@Override
public void cleanUp(ExecutionMonitor monitor, ExecutionContext context) throws InterruptedException, IOException {
ExecutionScriptHandler<T> target;
if (forceCleanUp != null) {
target = delegations.get(forceCleanUp);
} else {
target = resolve(context, null);
}
assert target != null;
YSLOG.info("I01003",
target.getHandlerId(),
context.getBatchId(),
context.getFlowId(),
context.getPhase(),
context.getExecutionId());
target.cleanUp(monitor, context);
}
private enum FindPattern {
STAGE {
@Override
String getKey(ExecutionContext context, ExecutionScript script) {
if (script == null) {
return null;
}
return MessageFormat.format(
"{0}.{1}.{2}",
context.getFlowId(),
context.getPhase().getSymbol(),
script.getId());
}
},
PHASE {
@Override
String getKey(ExecutionContext context, ExecutionScript script) {
return MessageFormat.format(
"{0}.{1}.{2}",
context.getFlowId(),
context.getPhase().getSymbol(),
WILDCARD);
}
},
FLOW {
@Override
String getKey(ExecutionContext context, ExecutionScript script) {
return MessageFormat.format(
"{0}.{1}",
context.getFlowId(),
WILDCARD);
}
},
BATCH {
@Override
String getKey(ExecutionContext context, ExecutionScript script) {
return WILDCARD;
}
},
;
private static final String WILDCARD = "*";
abstract String getKey(ExecutionContext context, ExecutionScript script);
}
}