/**
* Licensed to JumpMind Inc under one or more contributor
* license agreements. See the NOTICE file distributed
* with this work for additional information regarding
* copyright ownership. JumpMind Inc licenses this file
* to you under the GNU General Public License, version 3.0 (GPLv3)
* (the "License"); you may not use this file except in compliance
* with the License.
*
* You should have received a copy of the GNU General Public License,
* version 3.0 (GPLv3) along with this library; if not, see
* <http://www.gnu.org/licenses/>.
*
* 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.jumpmind.symmetric.service.impl;
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.jumpmind.db.platform.IDatabasePlatform;
import org.jumpmind.db.sql.ISqlTemplate;
import org.jumpmind.db.sql.ISqlTransaction;
import org.jumpmind.symmetric.SymmetricException;
import org.jumpmind.symmetric.common.ParameterConstants;
import org.jumpmind.symmetric.common.TableConstants;
import org.jumpmind.symmetric.db.ISymmetricDialect;
import org.jumpmind.symmetric.model.BatchAck;
import org.jumpmind.symmetric.model.IncomingBatch;
import org.jumpmind.symmetric.model.Node;
import org.jumpmind.symmetric.model.NodeSecurity;
import org.jumpmind.symmetric.model.OutgoingBatch;
import org.jumpmind.symmetric.model.OutgoingBatch.Status;
import org.jumpmind.symmetric.service.IAcknowledgeService;
import org.jumpmind.symmetric.service.IParameterService;
import org.jumpmind.symmetric.service.IService;
import org.jumpmind.symmetric.transport.IOutgoingWithResponseTransport;
import org.jumpmind.symmetric.transport.ITransportManager;
import org.jumpmind.util.AppUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
abstract public class AbstractService implements IService {
protected final Logger log = LoggerFactory.getLogger(getClass());
protected IParameterService parameterService;
protected ISymmetricDialect symmetricDialect;
protected ISqlTemplate sqlTemplate;
protected IDatabasePlatform platform;
protected String tablePrefix;
private ISqlMap sqlMap;
private Set<String> logOnce = new HashSet<String>();
public AbstractService(IParameterService parameterService, ISymmetricDialect symmetricDialect) {
this.symmetricDialect = symmetricDialect;
this.parameterService = parameterService;
this.tablePrefix = parameterService.getTablePrefix();
this.platform = symmetricDialect.getPlatform();
this.sqlTemplate = symmetricDialect.getPlatform().getSqlTemplate();
}
protected Date maxDate(Date... dates) {
Date date = null;
if (dates != null) {
for (Date d : dates) {
if (d != null) {
if (date == null || d.after(date)) {
date = d;
}
}
}
}
return date;
}
protected void setSqlMap(ISqlMap sqlMap) {
this.sqlMap = sqlMap;
}
public ISqlTemplate getJdbcTemplate() {
return symmetricDialect.getPlatform().getSqlTemplate();
}
synchronized public void synchronize(Runnable runnable) {
runnable.run();
}
protected boolean isSet(Object value) {
if (value != null && value.toString().equals("1")) {
return true;
} else {
return false;
}
}
@SuppressWarnings("unchecked")
protected SQLException unwrapSqlException(Throwable e) {
List<Throwable> exs = ExceptionUtils.getThrowableList(e);
for (Throwable throwable : exs) {
if (throwable instanceof SQLException) {
return (SQLException) throwable;
}
}
return null;
}
protected Map<String, String> createSqlReplacementTokens() {
Map<String, String> replacementTokens = createSqlReplacementTokens(this.tablePrefix, symmetricDialect.getPlatform()
.getDatabaseInfo().getDelimiterToken());
replacementTokens.putAll(symmetricDialect.getSqlReplacementTokens());
return replacementTokens;
}
protected static Map<String, String> createSqlReplacementTokens(String tablePrefix,
String quotedIdentifier) {
Map<String, String> map = new HashMap<String, String>();
List<String> tables = TableConstants.getTablesWithoutPrefix();
for (String table : tables) {
map.put(table, String.format("%s%s%s", tablePrefix,
StringUtils.isNotBlank(tablePrefix) ? "_" : "", table));
}
return map;
}
public String getSql(String... keys) {
if (sqlMap != null) {
return sqlMap.getSql(keys);
} else {
return null;
}
}
public IParameterService getParameterService() {
return parameterService;
}
public ISymmetricDialect getSymmetricDialect() {
return symmetricDialect;
}
public String getTablePrefix() {
return tablePrefix;
}
protected void close(ISqlTransaction transaction) {
if (transaction != null) {
transaction.close();
}
}
protected Set<String> toNodeIds(Set<Node> nodes) {
return toNodeIds(nodes, null);
}
protected Set<String> toNodeIds(Set<Node> nodes, Set<String> nodeIds) {
nodeIds = nodeIds == null ? new HashSet<String>(nodes.size()) : nodeIds;
for (Node node : nodes) {
nodeIds.add(node.getNodeId());
}
return nodeIds;
}
protected String getRootMessage(Exception ex) {
Throwable cause = ExceptionUtils.getRootCause(ex);
if (cause == null) {
cause = ex;
}
return cause.getMessage();
}
protected boolean isCalledFromSymmetricAdminTool() {
boolean adminTool = false;
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
for (StackTraceElement stackTraceElement : trace) {
adminTool |= stackTraceElement.getClassName().equals(
"org.jumpmind.symmetric.SymmetricAdmin");
}
return adminTool;
}
protected String buildBatchWhere(List<String> nodeIds, List<String> channels,
List<?> statuses) {
boolean containsErrorStatus = statuses.contains(OutgoingBatch.Status.ER)
|| statuses.contains(IncomingBatch.Status.ER);
boolean containsIgnoreStatus = statuses.contains(OutgoingBatch.Status.IG)
|| statuses.contains(IncomingBatch.Status.IG);
StringBuilder where = new StringBuilder();
boolean needsAnd = false;
if (nodeIds.size() > 0) {
where.append("node_id in (:NODES)");
needsAnd = true;
}
if (channels.size() > 0) {
if (needsAnd) {
where.append(" and ");
}
where.append("channel_id in (:CHANNELS)");
needsAnd = true;
}
if (statuses.size() > 0) {
if (needsAnd) {
where.append(" and ");
}
where.append("(status in (:STATUSES)");
if (containsErrorStatus) {
where.append(" or error_flag = 1 ");
}
if (containsIgnoreStatus) {
where.append(" or ignore_count > 0 ");
}
where.append(")");
needsAnd = true;
}
if (where.length() > 0) {
where.insert(0, " where ");
}
return where.toString();
}
/**
* Try a configured number of times to get the ACK through.
*/
protected void sendAck(Node remote, Node local, NodeSecurity localSecurity,
List<IncomingBatch> list, ITransportManager transportManager) throws IOException {
Exception error = null;
int sendAck = -1;
int numberOfStatusSendRetries = parameterService
.getInt(ParameterConstants.DATA_LOADER_NUM_OF_ACK_RETRIES);
for (int i = 0; i < numberOfStatusSendRetries && sendAck != HttpURLConnection.HTTP_OK; i++) {
try {
sendAck = transportManager.sendAcknowledgement(remote, list, local,
localSecurity.getNodePassword(), parameterService.getRegistrationUrl());
} catch (IOException ex) {
error = ex;
} catch (RuntimeException ex) {
error = ex;
}
if (sendAck != HttpURLConnection.HTTP_OK) {
log.warn("Ack was not sent successfully on try number {}. {}", i + 1,
error != null ? error.getMessage() : "");
if (i < numberOfStatusSendRetries - 1) {
AppUtils.sleep(parameterService
.getLong(ParameterConstants.DATA_LOADER_TIME_BETWEEN_ACK_RETRIES));
} else if (error instanceof RuntimeException) {
throw (RuntimeException) error;
} else if (error instanceof IOException) {
throw (IOException) error;
} else {
throw new IOException(Integer.toString(sendAck));
}
}
}
}
protected List<BatchAck> readAcks(List<OutgoingBatch> batches, IOutgoingWithResponseTransport transport,
ITransportManager transportManager, IAcknowledgeService acknowledgeService)
throws IOException {
Set<Long> batchIds = new HashSet<Long>(batches.size());
for (OutgoingBatch outgoingBatch : batches) {
if (outgoingBatch.getStatus() == OutgoingBatch.Status.LD) {
batchIds.add(outgoingBatch.getBatchId());
}
}
BufferedReader reader = transport.readResponse();
String ackString = reader.readLine();
String ackExtendedString = reader.readLine();
log.debug("Reading ack: {}", ackString);
log.debug("Reading extend ack: {}", ackExtendedString);
String line = null;
do {
line = reader.readLine();
if (line != null) {
log.info("Read another unexpected line {}", line);
}
} while (line != null);
if (StringUtils.isBlank(ackString)) {
throw new SymmetricException("Did not receive an acknowledgement for the batches sent. "
+ "The 'ack string' was: '%s' and the 'extended ack string' was: '%s'", ackString, ackExtendedString);
}
List<BatchAck> batchAcks = transportManager.readAcknowledgement(ackString,
ackExtendedString);
long batchIdInError = Long.MAX_VALUE;
for (BatchAck batchInfo : batchAcks) {
batchIds.remove(batchInfo.getBatchId());
if (batchInfo.getStatus() == Status.ER) {
batchIdInError = batchInfo.getBatchId();
}
log.debug("Saving ack: {}, {}", batchInfo.getBatchId(),
batchInfo.getStatus());
acknowledgeService.ack(batchInfo);
}
for (Long batchId : batchIds) {
if (batchId < batchIdInError) {
log.error("We expected but did not receive an ack for batch {}", batchId);
}
}
return batchAcks;
}
protected void logOnce(String message) {
if (!logOnce.contains(message)) {
logOnce.add(message);
log.info(message);
}
}
protected boolean isStreamClosedByClient(Exception ex) {
if (ExceptionUtils.indexOfType(ex, EOFException.class) >= 0) {
return true;
} else {
return false;
}
}
}