/**
* 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.web.rest;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.MDC;
import org.jumpmind.db.model.Table;
import org.jumpmind.db.sql.ISqlTemplate;
import org.jumpmind.db.sql.Row;
import org.jumpmind.exception.IoException;
import org.jumpmind.symmetric.ISymmetricEngine;
import org.jumpmind.symmetric.common.Constants;
import org.jumpmind.symmetric.common.ParameterConstants;
import org.jumpmind.symmetric.io.data.writer.StructureDataWriter.PayloadType;
import org.jumpmind.symmetric.model.BatchAck;
import org.jumpmind.symmetric.model.BatchAckResult;
import org.jumpmind.symmetric.model.IncomingBatch;
import org.jumpmind.symmetric.model.IncomingBatch.Status;
import org.jumpmind.symmetric.model.NetworkedNode;
import org.jumpmind.symmetric.model.NodeChannel;
import org.jumpmind.symmetric.model.NodeGroupLink;
import org.jumpmind.symmetric.model.NodeHost;
import org.jumpmind.symmetric.model.NodeSecurity;
import org.jumpmind.symmetric.model.OutgoingBatch;
import org.jumpmind.symmetric.model.OutgoingBatchSummary;
import org.jumpmind.symmetric.model.OutgoingBatchWithPayload;
import org.jumpmind.symmetric.model.ProcessInfo;
import org.jumpmind.symmetric.model.ProcessInfoKey;
import org.jumpmind.symmetric.model.ProcessType;
import org.jumpmind.symmetric.model.Trigger;
import org.jumpmind.symmetric.model.TriggerRouter;
import org.jumpmind.symmetric.service.IAcknowledgeService;
import org.jumpmind.symmetric.service.IConfigurationService;
import org.jumpmind.symmetric.service.IDataExtractorService;
import org.jumpmind.symmetric.service.IDataLoaderService;
import org.jumpmind.symmetric.service.IDataService;
import org.jumpmind.symmetric.service.INodeService;
import org.jumpmind.symmetric.service.IOutgoingBatchService;
import org.jumpmind.symmetric.service.IRegistrationService;
import org.jumpmind.symmetric.service.ITriggerRouterService;
import org.jumpmind.symmetric.statistic.IStatisticManager;
import org.jumpmind.symmetric.web.ServerSymmetricEngine;
import org.jumpmind.symmetric.web.SymmetricEngineHolder;
import org.jumpmind.symmetric.web.WebConstants;
import org.jumpmind.symmetric.web.rest.model.Batch;
import org.jumpmind.symmetric.web.rest.model.BatchAckResults;
import org.jumpmind.symmetric.web.rest.model.BatchResult;
import org.jumpmind.symmetric.web.rest.model.BatchResults;
import org.jumpmind.symmetric.web.rest.model.BatchSummaries;
import org.jumpmind.symmetric.web.rest.model.BatchSummary;
import org.jumpmind.symmetric.web.rest.model.ChannelStatus;
import org.jumpmind.symmetric.web.rest.model.Engine;
import org.jumpmind.symmetric.web.rest.model.EngineList;
import org.jumpmind.symmetric.web.rest.model.Heartbeat;
import org.jumpmind.symmetric.web.rest.model.Node;
import org.jumpmind.symmetric.web.rest.model.NodeList;
import org.jumpmind.symmetric.web.rest.model.NodeStatus;
import org.jumpmind.symmetric.web.rest.model.PullDataResults;
import org.jumpmind.symmetric.web.rest.model.QueryResults;
import org.jumpmind.symmetric.web.rest.model.RegistrationInfo;
import org.jumpmind.symmetric.web.rest.model.RestError;
import org.jumpmind.symmetric.web.rest.model.SendSchemaRequest;
import org.jumpmind.symmetric.web.rest.model.SendSchemaResponse;
import org.jumpmind.symmetric.web.rest.model.TableName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartFile;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
/**
* This is a REST API for SymmetricDS. The API will be active only if
* rest.api.enable=true. The property is turned off by default. The REST API is
* available at http://hostname:port/api for the stand alone SymmetricDS
* installation.
*
* <p>
* <b>General HTTP Responses to the methods:</b>
* <ul>
* <li>
* ALL Methods may return the following HTTP responses.<br>
* <br>
* In general:<br>
* <ul>
* <li>HTTP 2xx = Success</li>
* <li>HTTP 4xx = Problem on the caller (client) side</li>
* <li>HTTP 5xx - Problem on the REST service side</li>
* </ul>
* ALL Methods
* <ul>
* <li>HTTP 401 - Unauthorized. You have not successfully authenticated.
* Authentication details are in the response body.</li>
* <li>HTTP 404 - Not Found. You attempted to perform an operation on a resource
* that doesn't exist. I.E. you tried to start or stop an engine that doesn't
* exist.</li>
* <li>HTTP 405 - Method Not Allowed. I.E. you attempted a service call that
* uses the default engine (/engine/identity vs engine/{engine}/identity) and
* there was more than one engine found on the server.</li>
* <li>HTTP 500 - Internal Server Error. Something went wrong on the server /
* service, and we couldn't fulfill the request. Details are in the response
* body.</li>
* </ul>
* </li>
* <li>
* GET Methods
* <ul>
* <li>HTTP 200 - Success with result contained in the response body.</li>
* <li>HTTP 204 - Success with no results. Your GET request completed
* successfully, but found no matching entities.</li>
* </ul>
* </ul>
* </p>
*/
@Controller
public class RestService {
protected final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
ServletContext context;
/**
* Provides a list of {@link Engine} that are configured on the node.
*
* @return {@link EngineList} - Engines configured on the node <br>
*
* <pre>
* Example xml reponse is as follows:<br><br>
* {@code
* <enginelist>
* <engines>
* <name>RootSugarDB-root</name>
* </engines>
* </enginelist>
* }
* <br>
* Example json response is as follows:<br><br>
* {"engines":[{"name":"RootSugarDB-root"}]}
* </pre>
*/
@ApiOperation(value = "Obtain a list of configured Engines")
@RequestMapping(value = "/enginelist", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final EngineList getEngineList() {
EngineList list = new EngineList();
Collection<ServerSymmetricEngine> engines = getSymmetricEngineHolder().getEngines()
.values();
for (ISymmetricEngine engine : engines) {
if (engine.getParameterService().is(ParameterConstants.REST_API_ENABLED)) {
list.addEngine(new Engine(engine.getEngineName()));
}
}
return list;
}
/**
* Provides Node information for the single engine
*
* return {@link Node}<br>
*
* <pre>
* Example xml reponse is as follows:<br><br>
* {@code
* <node>
* <batchInErrorCount>0</batchInErrorCount>
* <batchToSendCount>0</batchToSendCount>
* <externalId>server01</externalId>
* <initialLoaded>true</initialLoaded>
* <lastHeartbeat>2012-12-20T09:26:02-05:00</lastHeartbeat>
* <name>server01</name>
* <registered>true</registered>
* <registrationServer>true</registrationServer>
* <reverseInitialLoaded>false</reverseInitialLoaded>
* <syncUrl>http://machine-name:31415/sync/RootSugarDB-root</syncUrl>
* </node>
* }
* <br>
* Example json response is as follows:<br><br>
* {"name":"server01","externalId":"server01","registrationServer":true,"syncUrl":"http://machine-name:31415/sync/RootSugarDB-root","batchToSendCount":0,"batchInErrorCount":0,"lastHeartbeat":1356013562000,"registered":true,"initialLoaded":true,"reverseInitialLoaded":false}
* </pre>
*/
@ApiOperation(value = "Obtain node information for the single engine")
@RequestMapping(value = "engine/node", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final Node getNode() {
return nodeImpl(getSymmetricEngine());
}
/**
* Provides Node information for the specified engine
*/
@ApiOperation(value = "Obtain node information for he specified engine")
@RequestMapping(value = "engine/{engine}/node", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final Node getNode(@PathVariable("engine") String engineName) {
return nodeImpl(getSymmetricEngine(engineName));
}
/**
* Provides a list of children that are registered with this engine.
*
* return {@link Node}<br>
*
* <pre>
* Example xml reponse is as follows:<br><br>
* {@code
* <nodelist>
* <nodes>
* <batchInErrorCount>0</batchInErrorCount>
* <batchToSendCount>0</batchToSendCount>
* <externalId>client01</externalId>
* <initialLoaded>true</initialLoaded>
* <name>client01</name>
* <registered>true</registered>
* <registrationServer>false</registrationServer>
* <reverseInitialLoaded>false</reverseInitialLoaded>
* <syncUrl>http://machine-name:31418/sync/ClientSugarDB-client01</syncUrl>
* </nodes>
* </nodelist>
* }
* <br>
* Example json response is as follows:<br><br>
* {"nodes":[{"name":"client01","externalId":"client01","registrationServer":false,"syncUrl":"http://gwilmer-laptop:31418/sync/ClientSugarDB-client01","batchToSendCount":0,"batchInErrorCount":0,"lastHeartbeat":null,"registered":true,"initialLoaded":true,"reverseInitialLoaded":false}]}
* </pre>
*/
@ApiOperation(value = "Obtain list of children for the single engine")
@RequestMapping(value = "engine/children", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final NodeList getChildren() {
return childrenImpl(getSymmetricEngine());
}
/**
* Provides a list of children {@link Node} that are registered with this
* engine.
*/
@ApiOperation(value = "Obtain list of children for the specified engine")
@RequestMapping(value = "engine/{engine}/children", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final NodeList getChildrenByEngine(@PathVariable("engine") String engineName) {
return childrenImpl(getSymmetricEngine(engineName));
}
/**
* Takes a snapshot for this engine and streams it to the client. The result
* of this call is a stream that should be written to a zip file. The zip
* contains configuration and operational information about the sureation
* and can be used to diagnose state of the node
*/
@ApiOperation(value = "Take a diagnostic snapshot for the single engine")
@RequestMapping(value = "engine/snapshot", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final void getSnapshot(HttpServletResponse resp) {
getSnapshot(getSymmetricEngine().getEngineName(), resp);
}
/**
* Executes a select statement on the node and returns results. <br>
* Example json response is as follows:<br>
* <br>
* {"nbrResults":1,"results":[{"rowNum":1,"columnData":[{"ordinal":1,"name":
* "node_id","value":"root"}]}]}
*
*/
@ApiOperation(value = "Execute the specified SQL statement on the single engine")
@RequestMapping(value = "engine/querynode", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final QueryResults getQueryNode(@RequestParam(value = "query") String sql) {
return queryNodeImpl(getSymmetricEngine(), sql);
}
/**
* Executes a select statement on the node and returns results.
*/
@ApiOperation(value = "Execute the specified SQL statement for the specified engine")
@RequestMapping(value = "engine/{engine}/querynode", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final QueryResults getQueryNode(@PathVariable("engine") String engineName,
@RequestParam(value = "query") String sql) {
return queryNodeImpl(getSymmetricEngine(engineName), sql);
}
/**
* Takes a snapshot for the specified engine and streams it to the client.
*/
@ApiOperation(value = "Take a diagnostic snapshot for the specified engine")
@RequestMapping(value = "engine/{engine}/snapshot", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final void getSnapshot(@PathVariable("engine") String engineName,
HttpServletResponse resp) {
BufferedInputStream bis = null;
try {
ISymmetricEngine engine = getSymmetricEngine(engineName);
File file = engine.snapshot();
resp.setHeader("Content-Disposition",
String.format("attachment; filename=%s", file.getName()));
bis = new BufferedInputStream(new FileInputStream(file));
IOUtils.copy(bis, resp.getOutputStream());
} catch (IOException e) {
throw new IoException(e);
} finally {
IOUtils.closeQuietly(bis);
}
}
/**
* Installs and starts a new node
*
* @param file
* A file stream that contains the node's properties.
*/
@ApiOperation(value = "Load a configuration file to the single engine")
@RequestMapping(value = "engine/install", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postInstall(@RequestParam MultipartFile file) {
try {
Properties properties = new Properties();
properties.load(file.getInputStream());
getSymmetricEngineHolder().install(properties);
} catch (RuntimeException ex) {
throw ex;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* Loads a configuration profile for the single engine on the node.
*
* @param file
* A file stream that contains the profile itself.
*/
@ApiOperation(value = "Load a configuration file to the single engine")
@RequestMapping(value = "engine/profile", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postProfile(@RequestParam MultipartFile file) {
loadProfileImpl(getSymmetricEngine(), file);
}
/**
* Loads a configuration profile for the specified engine on the node.
*
* @param file
* A file stream that contains the profile itself.
*/
@ApiOperation(value = "Load a configuration file to the specified engine")
@RequestMapping(value = "engine/{engine}/profile", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postProfileByEngine(@PathVariable("engine") String engineName,
@RequestParam(value = "file") MultipartFile file) {
loadProfileImpl(getSymmetricEngine(engineName), file);
}
/**
* Starts the single engine on the node
*/
@ApiOperation(value = "Start the single engine")
@RequestMapping(value = "engine/start", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postStart() {
startImpl(getSymmetricEngine());
}
/**
* Starts the specified engine on the node
*/
@ApiOperation(value = "Start the specified engine")
@RequestMapping(value = "engine/{engine}/start", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postStartByEngine(@PathVariable("engine") String engineName) {
startImpl(getSymmetricEngine(engineName));
}
/**
* Stops the single engine on the node
*/
@ApiOperation(value = "Stop the single engine")
@RequestMapping(value = "engine/stop", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postStop() {
stopImpl(getSymmetricEngine());
}
/**
* Stops the specified engine on the node
*/
@ApiOperation(value = "Stop the specified engine")
@RequestMapping(value = "engine/{engine}/stop", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postStopByEngine(@PathVariable("engine") String engineName) {
stopImpl(getSymmetricEngine(engineName));
}
/**
* Creates instances of triggers for each entry configured table/trigger for
* the single engine on the node
*/
@ApiOperation(value = "Sync triggers on the single engine")
@RequestMapping(value = "engine/synctriggers", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postSyncTriggers(
@RequestParam(required = false, value = "force") boolean force) {
syncTriggersImpl(getSymmetricEngine(), force);
}
/**
* Creates instances of triggers for each entry configured table/trigger for
* the specified engine on the node
*/
@ApiOperation(value = "Sync triggers on the specified engine")
@RequestMapping(value = "engine/{engine}/synctriggers", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postSyncTriggersByEngine(@PathVariable("engine") String engineName,
@RequestParam(required = false, value = "force") boolean force) {
syncTriggersImpl(getSymmetricEngine(engineName), force);
}
@ApiOperation(value = "Sync triggers on the single engine for a table")
@RequestMapping(value = "engine/synctriggers/{table}", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postSyncTriggersByTable(@PathVariable("table") String tableName,
@RequestParam(required = false, value = "catalog") String catalogName,
@RequestParam(required = false, value = "schema") String schemaName,
@RequestParam(required = false, value = "force") boolean force) {
syncTriggersByTableImpl(getSymmetricEngine(), catalogName, schemaName, tableName, force);
}
@ApiOperation(value = "Sync triggers on the specific engine for a table")
@RequestMapping(value = "engine/{engine}/synctriggers/{table}", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postSyncTriggersByTable(@PathVariable("engine") String engineName,
@PathVariable("table") String tableName,
@RequestParam(required = false, value = "catalog") String catalogName,
@RequestParam(required = false, value = "schema") String schemaName,
@RequestParam(required = false, value = "force") boolean force) {
syncTriggersByTableImpl(getSymmetricEngine(engineName), catalogName, schemaName, tableName,
force);
}
/**
* Send schema updates for all tables or a list of tables to a list of nodes
* or to all nodes in a group.
* <p>
* Example json request to send all tables to all nodes in group:<br>
* { "nodeGroupIdToSendTo": "target_group_name" }
* <p>
* Example json request to send all tables to a list of nodes:<br>
* { "nodeIdsToSendTo": [ "1", "2" ] }
* <p>
* Example json request to send a table to a list of nodes:<br>
* { "nodeIdsToSendTo": ["1", "2"], "tablesToSend": [ { "catalogName": "", "schemaName": "", "tableName": "A" } ] }
* <p>
* Example json response:
* { "nodeIdsSentTo": { "1": [ { "catalogName": null, "schemaName": null, "tableName": "A" } ] } }
*
* @param engineName
* @param request
* @return {@link SendSchemaResponse}
*/
@ApiOperation(value = "Send schema updates for all tables or a list of tables to a list of nodes or to all nodes in a group.")
@RequestMapping(value = "engine/{engine}/sendschema", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final SendSchemaResponse postSendSchema(@PathVariable("engine") String engineName,
@RequestBody SendSchemaRequest request) {
return sendSchemaImpl(getSymmetricEngine(engineName), request);
}
/**
* Send schema updates for all tables or a list of tables to a list of nodes
* or to all nodes in a group. See
* {@link RestService#postSendSchema(String, SendSchemaRequest)} for
* additional details.
*
* @param request
* @return {@link SendSchemaResponse}
*/
@ApiOperation(value = "Send schema updates for all tables or a list of tables to a list of nodes or to all nodes in a group.")
@RequestMapping(value = "engine/sendschema", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final SendSchemaResponse postSendSchema(@RequestBody SendSchemaRequest request) {
return sendSchemaImpl(getSymmetricEngine(), request);
}
/**
* Removes instances of triggers for each entry configured table/trigger for
* the single engine on the node
*/
@ApiOperation(value = "Drop triggers on the single engine")
@RequestMapping(value = "engine/droptriggers", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postDropTriggers() {
dropTriggersImpl(getSymmetricEngine());
}
/**
* Removes instances of triggers for each entry configured table/trigger for
* the specified engine on the node
*/
@ApiOperation(value = "Drop triggers on the specified engine")
@RequestMapping(value = "engine/{engine}/droptriggers", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postDropTriggersByEngine(@PathVariable("engine") String engineName) {
dropTriggersImpl(getSymmetricEngine(engineName));
}
/**
* Removes instances of triggers for the specified table for the single
* engine on the node
*/
@ApiOperation(value = "Drop triggers for the specified table on the single engine")
@RequestMapping(value = "engine/table/{table}/droptriggers", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postDropTriggersByTable(@PathVariable("table") String tableName) {
dropTriggersImpl(getSymmetricEngine(), tableName);
}
/**
* Removes instances of triggers for the specified table for the single
* engine on the node
*
*/
@ApiOperation(value = "Drop triggers for the specified table on the specified engine")
@RequestMapping(value = "engine/{engine}/table/{table}/droptriggers", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postDropTriggersByEngineByTable(@PathVariable("engine") String engineName,
@PathVariable("table") String tableName) {
dropTriggersImpl(getSymmetricEngine(engineName), tableName);
}
/**
* Uninstalls all SymmetricDS objects from the given node (database) for the
* single engine on the node
*/
@ApiOperation(value = "Uninstall SymmetricDS on the single engine")
@RequestMapping(value = "engine/uninstall", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postUninstall() {
uninstallImpl(getSymmetricEngine());
}
/**
* Uninstalls all SymmetricDS objects from the given node (database) for the
* specified engine on the node
*
*/
@ApiOperation(value = "Uninstall SymmetricDS on the specified engine")
@RequestMapping(value = "engine/{engine}/uninstall", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postUninstallByEngine(@PathVariable("engine") String engineName) {
uninstallImpl(getSymmetricEngine(engineName));
}
/**
* Reinitializes the given node (database) for the single engine on the node
*/
@ApiOperation(value = "Reinitiailize SymmetricDS on the single engine")
@RequestMapping(value = "engine/reinitialize", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postReinitialize() {
reinitializeImpl(getSymmetricEngine());
}
/**
* Reinitializes the given node (database) for the specified engine on the
* node
*
*/
@ApiOperation(value = "Reinitiailize SymmetricDS on the specified engine")
@RequestMapping(value = "engine/{engine}/reinitialize", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postReinitializeByEngine(@PathVariable("engine") String engineName) {
reinitializeImpl(getSymmetricEngine(engineName));
}
/**
* Refreshes cache for the single engine on the node
*/
@ApiOperation(value = "Refresh caches on the single engine")
@RequestMapping(value = "engine/refreshcache", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postClearCaches() {
clearCacheImpl(getSymmetricEngine());
}
/**
* Refreshes cache for the specified engine on the node node
*
*/
@ApiOperation(value = "Refresh caches on the specified engine")
@RequestMapping(value = "engine/{engine}/refreshcache", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postClearCachesByEngine(@PathVariable("engine") String engineName) {
clearCacheImpl(getSymmetricEngine(engineName));
}
/**
* Returns an overall status for the single engine of the node.
*
* @return {@link NodeStatus}
*
* <pre>
* Example xml reponse is as follows:<br><br>
* {@code
* <nodestatus>
* <batchInErrorCount>0</batchInErrorCount>
* <batchToSendCount>0</batchToSendCount>
* <databaseType>Microsoft SQL Server</databaseType>
* <databaseVersion>9.0</databaseVersion>
* <deploymentType>professional</deploymentType>
* <externalId>root</externalId>
* <initialLoaded>true</initialLoaded>
* <lastHeartbeat>2012-11-17 14:52:19.267</lastHeartbeat>
* <nodeGroupId>RootSugarDB</nodeGroupId>
* <nodeId>root</nodeId>
* <registered>true</registered>
* <registrationServer>false</registrationServer>
* <started>true</started>
* <symmetricVersion>3.1.10</symmetricVersion>
* <syncEnabled>true</syncEnabled>
* <syncUrl>http://my-machine-name:31415/sync/RootSugarDB-root</syncUrl>
* </nodestatus>
* }
* <br>
* Example json response is as follows:<br><br>
* {"started":true,"registered":true,"registrationServer":false,"initialLoaded":true,
* "nodeId":"root","nodeGroupId":"RootSugarDB","externalId":"root",
* "syncUrl":"http://my-machine-name:31415/sync/RootSugarDB-root","databaseType":"Microsoft SQL Server",
* "databaseVersion":"9.0","syncEnabled":true,"createdAtNodeId":null,"batchToSendCount":0,
* "batchInErrorCount":0,"deploymentType":"professional","symmetricVersion":"3.1.10",
* "lastHeartbeat":"2012-11-17 15:15:00.033","hearbeatInterval":null}
* </pre>
*/
@ApiOperation(value = "Obtain the status of the single engine")
@RequestMapping(value = "/engine/status", method = RequestMethod.GET)
@ResponseBody
public final NodeStatus getStatus() {
return nodeStatusImpl(getSymmetricEngine());
}
/**
* Returns an overall status for the specified engine of the node.
*
* @return {@link NodeStatus}
*/
@ApiOperation(value = "Obtain the status of the specified engine")
@RequestMapping(value = "/engine/{engine}/status", method = RequestMethod.GET)
@ResponseBody
public final NodeStatus getStatusByEngine(@PathVariable("engine") String engineName) {
return nodeStatusImpl(getSymmetricEngine(engineName));
}
/**
* Returns status of each channel for the single engine of the node.
*
* @return Set<{@link ChannelStatus}>
*/
@ApiOperation(value = "Obtain the channel status of the single engine")
@RequestMapping(value = "/engine/channelstatus", method = RequestMethod.GET)
@ResponseBody
public final Set<ChannelStatus> getChannelStatus() {
return channelStatusImpl(getSymmetricEngine());
}
/**
* Returns status of each channel for the specified engine of the node.
*
* @return Set<{@link ChannelStatus}>
*/
@ApiOperation(value = "Obtain the channel status of the specified engine")
@RequestMapping(value = "/engine/{engine}/channelstatus", method = RequestMethod.GET)
@ResponseBody
public final Set<ChannelStatus> getChannelStatusByEngine(
@PathVariable("engine") String engineName) {
return channelStatusImpl(getSymmetricEngine(engineName));
}
/**
* Removes (unregisters and cleans up) a node for the single engine
*/
@ApiOperation(value = "Remove specified node (unregister and clean up) for the single engine")
@RequestMapping(value = "/engine/removenode", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postRemoveNode(@RequestParam(value = "nodeId") String nodeId) {
postRemoveNodeByEngine(nodeId, getSymmetricEngine().getEngineName());
}
/**
* Removes (unregisters and cleans up) a node for the single engine
*/
@ApiOperation(value = "Remove specified node (unregister and clean up) for the specified engine")
@RequestMapping(value = "/engine/{engine}/removenode", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postRemoveNodeByEngine(@RequestParam(value = "nodeId") String nodeId,
@PathVariable("engine") String engineName) {
getSymmetricEngine(engineName).removeAndCleanupNode(nodeId);
}
/**
* Requests the server to add this node to the synchronization scenario as a
* "pull only" node
*
* @param externalId
* The external id for this node
* @param nodeGroup
* The node group to which this node belongs
* @param databaseType
* The database type for this node
* @param databaseVersion
* The database version for this node
* @param hostName
* The host name of the machine on which the client is running
* @return {@link RegistrationInfo}
*
* <pre>
* Example json response is as follows:<br/><br/>
* {"registered":false,"nodeId":null,"syncUrl":null,"nodePassword":null}<br>
* In the above example, the node attempted to register, but was not able to successfully register
* because registration was not open on the server. Checking the "registered" element will allow you
* to determine whether the node was successfully registered.<br/><br/>
* The following example shows the results from the registration after registration has been opened
* on the server for the given node.<br/><br/>
* {"registered":true,"nodeId":"001","syncUrl":"http://myserverhost:31415/sync/server-000","nodePassword":"1880fbffd2bc2d00e1d58bd0c734ff"}<br/>
* The nodeId, syncUrl and nodePassword should be stored for subsequent calls to the REST API.
* </pre>
*/
@ApiOperation(value = "Register the specified node for the single engine")
@RequestMapping(value = "/engine/registernode", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final RegistrationInfo postRegisterNode(
@RequestParam(value = "externalId") String externalId,
@RequestParam(value = "nodeGroupId") String nodeGroupId,
@RequestParam(value = "databaseType") String databaseType,
@RequestParam(value = "databaseVersion") String databaseVersion,
@RequestParam(value = "hostName") String hostName) {
return postRegisterNode(getSymmetricEngine().getEngineName(), externalId, nodeGroupId,
databaseType, databaseVersion, hostName);
}
@ApiOperation(value = "Register the specified node for the specified engine")
@RequestMapping(value = "/engine/{engine}/registernode", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final RegistrationInfo postRegisterNode(@PathVariable("engine") String engineName,
@RequestParam(value = "externalId") String externalId,
@RequestParam(value = "nodeGroupId") String nodeGroupId,
@RequestParam(value = "databaseType") String databaseType,
@RequestParam(value = "databaseVersion") String databaseVersion,
@RequestParam(value = "hostName") String hostName) {
ISymmetricEngine engine = getSymmetricEngine(engineName);
IRegistrationService registrationService = engine.getRegistrationService();
INodeService nodeService = engine.getNodeService();
RegistrationInfo regInfo = new org.jumpmind.symmetric.web.rest.model.RegistrationInfo();
try {
org.jumpmind.symmetric.model.Node processedNode = registrationService
.registerPullOnlyNode(externalId, nodeGroupId, databaseType, databaseVersion);
regInfo.setRegistered(processedNode.isSyncEnabled());
if (regInfo.isRegistered()) {
regInfo.setNodeId(processedNode.getNodeId());
NodeSecurity nodeSecurity = nodeService.findNodeSecurity(processedNode.getNodeId());
regInfo.setNodePassword(nodeSecurity.getNodePassword());
org.jumpmind.symmetric.model.Node modelNode = nodeService.findIdentity();
regInfo.setSyncUrl(modelNode.getSyncUrl());
// do an initial heartbeat
Heartbeat heartbeat = new Heartbeat();
heartbeat.setNodeId(regInfo.getNodeId());
heartbeat.setHostName(hostName);
Date now = new Date();
heartbeat.setCreateTime(now);
heartbeat.setLastRestartTime(now);
heartbeat.setHeartbeatTime(now);
this.heartbeatImpl(engine, heartbeat);
}
// TODO: Catch a RegistrationRedirectException and redirect.
} catch (IOException e) {
throw new IoException(e);
}
return regInfo;
}
/**
* Pulls pending batches (data) for a given node.
*
* @param nodeId
* The node id of the node requesting to pull data
* @param securityToken
* The security token or password used to authenticate the pull.
* The security token is provided during the registration
* process.
* @param useJdbcTimestampFormat
* @param useUpsertStatements
* @param useDelimitedIdentifiers
* @param hostName
* The name of the host machine requesting the pull. Only
* required if you have the rest heartbeat on pull paramter set.
* @return {@link PullDataResults}
*
* Example json response is as follows:<br/>
* <br/>
* {"nbrBatches":2,"batches":[{"batchId":20,"sqlStatements":[
* "insert into table1 (field1, field2) values (value1,value2);"
* ,"update table1 set field1=value1;"
* ]},{"batchId":21,"sqlStatements"
* :["insert into table2 (field1, field2) values (value1,value2);"
* ,"update table2 set field1=value1;"]}]}<BR>
* <br/>
* If there are no batches to be pulled, the json response will look
* as follows:<br/>
* <br/>
* {"nbrBatches":0,"batches":[]} </pre>
*/
@ApiOperation(value = "Pull pending batches for the specified node for the single engine")
@RequestMapping(value = "/engine/pulldata", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final PullDataResults getPullData(
@RequestParam(value = WebConstants.NODE_ID) String nodeId,
@ApiParam(value="This the password for the nodeId being passed in. The password is stored in the node_security table")
@RequestParam(value = WebConstants.SECURITY_TOKEN) String securityToken,
@RequestParam(value = "useJdbcTimestampFormat", required = false, defaultValue = "true") boolean useJdbcTimestampFormat,
@RequestParam(value = "useUpsertStatements", required = false, defaultValue = "false") boolean useUpsertStatements,
@RequestParam(value = "useDelimitedIdentifiers", required = false, defaultValue = "true") boolean useDelimitedIdentifiers,
@RequestParam(value = "hostName", required = false) String hostName) {
return getPullData(getSymmetricEngine().getEngineName(), nodeId, securityToken,
useJdbcTimestampFormat, useUpsertStatements, useDelimitedIdentifiers, hostName);
}
@ApiOperation(value = "Pull pending batches for the specified node for the specified engine")
@RequestMapping(value = "/engine/{engine}/pulldata", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final PullDataResults getPullData(
@PathVariable("engine") String engineName,
@RequestParam(value = WebConstants.NODE_ID) String nodeId,
@ApiParam(value="This the password for the nodeId being passed in. The password is stored in the node_security table.")
@RequestParam(value = WebConstants.SECURITY_TOKEN) String securityToken,
@RequestParam(value = "useJdbcTimestampFormat", required = false, defaultValue = "true") boolean useJdbcTimestampFormat,
@RequestParam(value = "useUpsertStatements", required = false, defaultValue = "false") boolean useUpsertStatements,
@RequestParam(value = "useDelimitedIdentifiers", required = false, defaultValue = "true") boolean useDelimitedIdentifiers,
@RequestParam(value = "hostName", required = false) String hostName) {
ISymmetricEngine engine = getSymmetricEngine(engineName);
IDataExtractorService dataExtractorService = engine.getDataExtractorService();
IStatisticManager statisticManager = engine.getStatisticManager();
INodeService nodeService = engine.getNodeService();
org.jumpmind.symmetric.model.Node targetNode = nodeService.findNode(nodeId);
if (securityVerified(nodeId, engine, securityToken)) {
ProcessInfo processInfo = statisticManager.newProcessInfo(new ProcessInfoKey(
nodeService.findIdentityNodeId(), nodeId, ProcessType.REST_PULL_HANLDER));
try {
PullDataResults results = new PullDataResults();
List<OutgoingBatchWithPayload> extractedBatches = dataExtractorService
.extractToPayload(processInfo, targetNode, PayloadType.SQL,
useJdbcTimestampFormat, useUpsertStatements,
useDelimitedIdentifiers);
List<Batch> batches = new ArrayList<Batch>();
for (OutgoingBatchWithPayload outgoingBatchWithPayload : extractedBatches) {
if (outgoingBatchWithPayload.getStatus() == org.jumpmind.symmetric.model.OutgoingBatch.Status.LD
|| outgoingBatchWithPayload.getStatus() == org.jumpmind.symmetric.model.OutgoingBatch.Status.IG) {
Batch batch = new Batch();
batch.setBatchId(outgoingBatchWithPayload.getBatchId());
batch.setChannelId(outgoingBatchWithPayload.getChannelId());
batch.setSqlStatements(outgoingBatchWithPayload.getPayload());
batches.add(batch);
}
}
results.setBatches(batches);
results.setNbrBatches(batches.size());
processInfo.setStatus(org.jumpmind.symmetric.model.ProcessInfo.Status.OK);
if (engine.getParameterService().is(ParameterConstants.REST_HEARTBEAT_ON_PULL)
&& hostName != null) {
Heartbeat heartbeat = new Heartbeat();
heartbeat.setNodeId(nodeId);
heartbeat.setHeartbeatTime(new Date());
heartbeat.setHostName(hostName);
this.heartbeatImpl(engine, heartbeat);
}
return results;
} finally {
if (processInfo.getStatus() != org.jumpmind.symmetric.model.ProcessInfo.Status.OK) {
processInfo.setStatus(org.jumpmind.symmetric.model.ProcessInfo.Status.ERROR);
}
}
} else {
throw new NotAllowedException();
}
}
/**
* Sends a heartbeat to the server for the given node.
*
* @param nodeID
* - Required - The client nodeId this to which this heartbeat
* belongs See {@link Heartbeat} for request body requirements
*/
@ApiOperation(value = "Send a heartbeat for the single engine")
@RequestMapping(value = "/engine/heartbeat", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void putHeartbeat(
@ApiParam(value="This the password for the nodeId being passed in. The password is stored in the node_security table.")
@RequestParam(value = WebConstants.SECURITY_TOKEN) String securityToken,
@RequestBody Heartbeat heartbeat) {
if (securityVerified(heartbeat.getNodeId(), getSymmetricEngine(), securityToken)) {
putHeartbeat(getSymmetricEngine().getEngineName(), securityToken, heartbeat);
} else {
throw new NotAllowedException();
}
}
/**
* Sends a heartbeat to the server for the given node.
*
* @param nodeID
* - Required - The client nodeId this to which this heartbeat
* belongs See {@link Heartbeat} for request body requirements
*/
@ApiOperation(value = "Send a heartbeat for the specified engine")
@RequestMapping(value = "/engine/{engine}/heartbeat", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void putHeartbeat(@PathVariable("engine") String engineName,
@ApiParam(value="This the password for the nodeId being passed in. The password is stored in the node_security table.")
@RequestParam(value = WebConstants.SECURITY_TOKEN) String securityToken,
@RequestBody Heartbeat heartbeat) {
ISymmetricEngine engine = getSymmetricEngine(engineName);
if (securityVerified(heartbeat.getNodeId(), engine, securityToken)) {
heartbeatImpl(engine, heartbeat);
} else {
throw new NotAllowedException();
}
}
private void heartbeatImpl(ISymmetricEngine engine, Heartbeat heartbeat) {
INodeService nodeService = engine.getNodeService();
NodeHost nodeHost = new NodeHost();
if (heartbeat.getAvailableProcessors() != null) {
nodeHost.setAvailableProcessors(heartbeat.getAvailableProcessors());
}
if (heartbeat.getCreateTime() != null) {
nodeHost.setCreateTime(heartbeat.getCreateTime());
}
if (heartbeat.getFreeMemoryBytes() != null) {
nodeHost.setFreeMemoryBytes(heartbeat.getFreeMemoryBytes());
}
if (heartbeat.getHeartbeatTime() != null) {
nodeHost.setHeartbeatTime(heartbeat.getHeartbeatTime());
}
if (heartbeat.getHostName() != null) {
nodeHost.setHostName(heartbeat.getHostName());
}
if (heartbeat.getIpAddress() != null) {
nodeHost.setIpAddress(heartbeat.getIpAddress());
}
if (heartbeat.getJavaVendor() != null) {
nodeHost.setJavaVendor(heartbeat.getJavaVendor());
}
if (heartbeat.getJdbcVersion() != null) {
nodeHost.setJdbcVersion(heartbeat.getJdbcVersion());
}
if (heartbeat.getJavaVersion() != null) {
nodeHost.setJavaVersion(heartbeat.getJavaVersion());
}
if (heartbeat.getLastRestartTime() != null) {
nodeHost.setLastRestartTime(heartbeat.getLastRestartTime());
}
if (heartbeat.getMaxMemoryBytes() != null) {
nodeHost.setMaxMemoryBytes(heartbeat.getMaxMemoryBytes());
}
if (heartbeat.getNodeId() != null) {
nodeHost.setNodeId(heartbeat.getNodeId());
}
if (heartbeat.getOsArchitecture() != null) {
nodeHost.setOsArch(heartbeat.getOsArchitecture());
}
if (heartbeat.getOsName() != null) {
nodeHost.setOsName(heartbeat.getOsName());
}
if (heartbeat.getOsUser() != null) {
nodeHost.setOsUser(heartbeat.getOsUser());
}
if (heartbeat.getOsVersion() != null) {
nodeHost.setOsVersion(heartbeat.getOsVersion());
}
if (heartbeat.getSymmetricVersion() != null) {
nodeHost.setSymmetricVersion(heartbeat.getSymmetricVersion());
}
if (heartbeat.getTimezoneOffset() != null) {
nodeHost.setTimezoneOffset(heartbeat.getTimezoneOffset());
}
if (heartbeat.getTotalMemoryBytes() != null) {
nodeHost.setTotalMemoryBytes(heartbeat.getTotalMemoryBytes());
}
nodeService.updateNodeHost(nodeHost);
}
/**
* Acknowledges a set of batches that have been pulled and processed on the
* client side. Setting the status to OK will render the batch complete.
* Setting the status to anything other than OK will queue the batch on the
* server to be sent again on the next pull. if the status is "ER". In error
* status the status description should contain relevant information about
* the error on the client including SQL Error Number and description
*/
@ApiOperation(value = "Acknowledge a set of batches for the single engine")
@RequestMapping(value = "/engine/acknowledgebatch", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final BatchAckResults putAcknowledgeBatch(
@ApiParam(value="This the password for the nodeId being passed in. The password is stored in the node_security table.")
@RequestParam(value = WebConstants.SECURITY_TOKEN) String securityToken,
@RequestBody BatchResults batchResults) {
BatchAckResults results = putAcknowledgeBatch(getSymmetricEngine().getEngineName(),
securityToken, batchResults);
return results;
}
@ApiOperation(value = "Acknowledge a set of batches for the specified engine")
@RequestMapping(value = "/engine/{engine}/acknowledgebatch", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final BatchAckResults putAcknowledgeBatch(@PathVariable("engine") String engineName,
@ApiParam(value="This the password for the nodeId being passed in. The password is stored in the node_security table.")
@RequestParam(value = WebConstants.SECURITY_TOKEN) String securityToken,
@RequestBody BatchResults batchResults) {
BatchAckResults finalResult = new BatchAckResults();
ISymmetricEngine engine = getSymmetricEngine(engineName);
List<BatchAckResult> results = null;
if (batchResults.getBatchResults().size() > 0) {
if (securityVerified(batchResults.getNodeId(), engine, securityToken)) {
IAcknowledgeService ackService = engine.getAcknowledgeService();
List<BatchAck> batchAcks = convertBatchResultsToAck(batchResults);
results = ackService.ack(batchAcks);
} else {
throw new NotAllowedException();
}
}
finalResult.setBatchAckResults(results);
return finalResult;
}
private List<BatchAck> convertBatchResultsToAck(BatchResults batchResults) {
BatchAck batchAck = null;
List<BatchAck> batchAcks = new ArrayList<BatchAck>();
long transferTimeInMillis = batchResults.getTransferTimeInMillis();
if (transferTimeInMillis > 0) {
transferTimeInMillis = transferTimeInMillis / batchResults.getBatchResults().size();
}
for (BatchResult batchResult : batchResults.getBatchResults()) {
batchAck = new BatchAck(batchResult.getBatchId());
batchAck.setNodeId(batchResults.getNodeId());
batchAck.setNetworkMillis(transferTimeInMillis);
batchAck.setDatabaseMillis(batchResult.getLoadTimeInMillis());
if (batchResult.getStatus().equalsIgnoreCase("OK")) {
batchAck.setStatus(OutgoingBatch.Status.OK);
} else {
batchAck.setStatus(OutgoingBatch.Status.ER);
batchAck.setSqlCode(batchResult.getSqlCode());
batchAck.setSqlState(batchResult.getSqlState().substring(0,
Math.min(batchResult.getSqlState().length(), 10)));
batchAck.setSqlMessage(batchResult.getStatusDescription());
}
batchAcks.add(batchAck);
}
return batchAcks;
}
/**
* Requests an initial load from the server for the node id provided. The
* initial load requst directs the server to queue up initial load data for
* the client node. Data is obtained for the initial load by the client
* calling the pull method.
*
* @param nodeID
*/
@ApiOperation(value = "Request an initial load for the specified node for the single engine")
@RequestMapping(value = "/engine/requestinitialload", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postRequestInitialLoad(@RequestParam(value = "nodeId") String nodeId) {
postRequestInitialLoad(getSymmetricEngine().getEngineName(), nodeId);
}
/**
* Requests an initial load from the server for the node id provided. The
* initial load requst directs the server to queue up initial load data for
* the client node. Data is obtained for the initial load by the client
* calling the pull method.
*
* @param nodeID
*/
@ApiOperation(value = "Request an initial load for the specified node for the specified engine")
@RequestMapping(value = "/engine/{engine}/requestinitialload", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseBody
public final void postRequestInitialLoad(@PathVariable("engine") String engineName,
@RequestParam(value = "nodeId") String nodeId) {
ISymmetricEngine engine = getSymmetricEngine(engineName);
INodeService nodeService = engine.getNodeService();
nodeService.setInitialLoadEnabled(nodeId, true, false, -1, "restapi");
}
@ApiOperation(value = "Outgoing summary of batches and data counts waiting for a node")
@RequestMapping(value = "/engine/outgoingBatchSummary", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final BatchSummaries getOutgoingBatchSummary(
@RequestParam(value = WebConstants.NODE_ID) String nodeId,
@ApiParam(value="This the password for the nodeId being passed in. The password is stored in the node_security table.")
@RequestParam(value = WebConstants.SECURITY_TOKEN) String securityToken) {
return getOutgoingBatchSummary(getSymmetricEngine().getEngineName(), nodeId, securityToken);
}
@ApiOperation(value = "Outgoing summary of batches and data counts waiting for a node")
@RequestMapping(value = "/engine/{engine}/outgoingBatchSummary", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final BatchSummaries getOutgoingBatchSummary(
@PathVariable("engine") String engineName,
@RequestParam(value = WebConstants.NODE_ID) String nodeId,
@ApiParam(value="This the password for the nodeId being passed in. The password is stored in the node_security table.")
@RequestParam(value = WebConstants.SECURITY_TOKEN) String securityToken) {
ISymmetricEngine engine = getSymmetricEngine(engineName);
if (securityVerified(nodeId, engine, securityToken)) {
BatchSummaries summaries = new BatchSummaries();
summaries.setNodeId(nodeId);
IOutgoingBatchService outgoingBatchService = engine.getOutgoingBatchService();
List<OutgoingBatchSummary> list = outgoingBatchService.findOutgoingBatchSummary(
OutgoingBatch.Status.RQ, OutgoingBatch.Status.QY, OutgoingBatch.Status.NE,
OutgoingBatch.Status.SE, OutgoingBatch.Status.LD, OutgoingBatch.Status.ER);
for (OutgoingBatchSummary sum : list) {
if (sum.getNodeId().equals(nodeId)) {
BatchSummary summary = new BatchSummary();
summary.setBatchCount(sum.getBatchCount());
summary.setDataCount(sum.getDataCount());
summary.setOldestBatchCreateTime(sum.getOldestBatchCreateTime());
summary.setStatus(sum.getStatus().name());
summaries.getBatchSummaries().add(summary);
}
}
return summaries;
} else {
throw new NotAllowedException();
}
}
@ApiOperation(value = "Read parameter value")
@RequestMapping(value = "engine/parameter/{name}", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final String getParameter(@PathVariable("name") String name) {
return getSymmetricEngine().getParameterService().getString(name.replace('_', '.'));
}
@ApiOperation(value = "Read paramater value for the specified engine")
@RequestMapping(value = "engine/{engine}/parameter/{name}", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public final String getParameter(@PathVariable("engine") String engineName, @PathVariable("name") String name) {
return getSymmetricEngine(engineName).getParameterService().getString(name.replace('_', '.'));
}
@ExceptionHandler(Exception.class)
@ResponseBody
public RestError handleError(Exception ex, HttpServletRequest req) {
int httpErrorCode = 500;
Annotation annotation = ex.getClass().getAnnotation(ResponseStatus.class);
if (annotation != null) {
httpErrorCode = ((ResponseStatus) annotation).value().value();
}
return new RestError(ex, httpErrorCode);
}
private void startImpl(ISymmetricEngine engine) {
engine.getParameterService().saveParameter(ParameterConstants.AUTO_START_ENGINE, "true", Constants.SYSTEM_USER);
if (engine.start()) {
throw new InternalServerErrorException();
}
}
private void stopImpl(ISymmetricEngine engine) {
engine.stop();
engine.getParameterService().saveParameter(ParameterConstants.AUTO_START_ENGINE, "false", Constants.SYSTEM_USER);
}
private void syncTriggersImpl(ISymmetricEngine engine, boolean force) {
ITriggerRouterService triggerRouterService = engine.getTriggerRouterService();
StringBuilder buffer = new StringBuilder();
triggerRouterService.syncTriggers(buffer, force);
}
private void syncTriggersByTableImpl(ISymmetricEngine engine, String catalogName,
String schemaName, String tableName, boolean force) {
ITriggerRouterService triggerRouterService = engine.getTriggerRouterService();
Table table = getSymmetricEngine().getDatabasePlatform().getTableFromCache(catalogName,
schemaName, tableName, true);
if (table == null) {
throw new NotFoundException();
}
triggerRouterService.syncTriggers(table, force);
}
private void dropTriggersImpl(ISymmetricEngine engine) {
ITriggerRouterService triggerRouterService = engine.getTriggerRouterService();
triggerRouterService.dropTriggers();
}
private void dropTriggersImpl(ISymmetricEngine engine, String tableName) {
ITriggerRouterService triggerRouterService = engine.getTriggerRouterService();
HashSet<String> tables = new HashSet<String>();
tables.add(tableName);
triggerRouterService.dropTriggers(tables);
}
private SendSchemaResponse sendSchemaImpl(ISymmetricEngine engine, SendSchemaRequest request) {
IConfigurationService configurationService = engine.getConfigurationService();
INodeService nodeService = engine.getNodeService();
ITriggerRouterService triggerRouterService = engine.getTriggerRouterService();
IDataService dataService = engine.getDataService();
SendSchemaResponse response = new SendSchemaResponse();
org.jumpmind.symmetric.model.Node identity = nodeService.findIdentity();
if (identity != null) {
List<org.jumpmind.symmetric.model.Node> nodesToSendTo = new ArrayList<org.jumpmind.symmetric.model.Node>();
List<String> nodeIds = request.getNodeIdsToSendTo();
if (nodeIds == null || nodeIds.size() == 0) {
nodeIds = new ArrayList<String>();
String nodeGroupIdToSendTo = request.getNodeGroupIdToSendTo();
if (isNotBlank(nodeGroupIdToSendTo)) {
NodeGroupLink link = configurationService.getNodeGroupLinkFor(
identity.getNodeGroupId(), nodeGroupIdToSendTo, false);
if (link != null) {
Collection<org.jumpmind.symmetric.model.Node> nodes = nodeService
.findEnabledNodesFromNodeGroup(nodeGroupIdToSendTo);
nodesToSendTo.addAll(nodes);
} else {
log.warn("Could not send schema to all nodes in the '"
+ nodeGroupIdToSendTo + "' node group. No node group link exists");
}
} else {
log.warn("Could not send schema to nodes. There are none that were provided and the nodeGroupIdToSendTo was also not provided");
}
} else {
for (String nodeIdToValidate : nodeIds) {
org.jumpmind.symmetric.model.Node node = nodeService.findNode(nodeIdToValidate);
if (node != null) {
NodeGroupLink link = configurationService.getNodeGroupLinkFor(
identity.getNodeGroupId(), node.getNodeGroupId(), false);
if (link != null) {
nodesToSendTo.add(node);
} else {
log.warn("Could not send schema to node '" + nodeIdToValidate
+ "'. No node group link exists");
}
} else {
log.warn("Could not send schema to node '" + nodeIdToValidate
+ "'. It was not present in the database");
}
}
}
Map<String, List<TableName>> results = response.getNodeIdsSentTo();
List<String> nodeIdsToSendTo = toNodeIds(nodesToSendTo);
for (String nodeId : nodeIdsToSendTo) {
results.put(nodeId, new ArrayList<TableName>());
}
if (nodesToSendTo.size() > 0) {
List<TableName> tablesToSend = request.getTablesToSend();
List<TriggerRouter> triggerRouters = triggerRouterService.getTriggerRouters(true, false);
for (TriggerRouter triggerRouter : triggerRouters) {
Trigger trigger = triggerRouter.getTrigger();
NodeGroupLink link = triggerRouter.getRouter().getNodeGroupLink();
if (link.getSourceNodeGroupId().equals(identity.getNodeGroupId())) {
for (org.jumpmind.symmetric.model.Node node : nodesToSendTo) {
if (link.getTargetNodeGroupId().equals(node.getNodeGroupId())) {
if (tablesToSend == null || tablesToSend.size() == 0
|| contains(trigger, tablesToSend)) {
dataService.sendSchema(node.getNodeId(),
trigger.getSourceCatalogName(),
trigger.getSourceSchemaName(),
trigger.getSourceTableName(), false);
results.get(node.getNodeId()).add(
new TableName(trigger.getSourceCatalogName(), trigger
.getSourceSchemaName(), trigger
.getSourceTableName()));
}
}
}
}
}
}
}
return response;
}
private boolean contains(Trigger trigger, List<TableName> tables) {
for (TableName tableName : tables) {
if (trigger.getFullyQualifiedSourceTableName().equals(
Table.getFullyQualifiedTableName(tableName.getCatalogName(),
tableName.getSchemaName(), tableName.getTableName()))) {
return true;
}
}
return false;
}
private List<String> toNodeIds(List<org.jumpmind.symmetric.model.Node> nodes) {
List<String> nodeIds = new ArrayList<String>(nodes.size());
for (org.jumpmind.symmetric.model.Node node : nodes) {
nodeIds.add(node.getNodeId());
}
return nodeIds;
}
private void uninstallImpl(ISymmetricEngine engine) {
engine.uninstall();
}
private void reinitializeImpl(ISymmetricEngine engine) {
INodeService nodeService = engine.getNodeService();
org.jumpmind.symmetric.model.Node modelNode = nodeService.findIdentity();
if (!this.isRootNode(engine, modelNode)) {
engine.uninstall();
}
engine.start();
}
private void clearCacheImpl(ISymmetricEngine engine) {
engine.clearCaches();
}
private void loadProfileImpl(ISymmetricEngine engine, MultipartFile file) {
IDataLoaderService dataLoaderService = engine.getDataLoaderService();
boolean inError = false;
try {
String content = new String(file.getBytes());
List<IncomingBatch> batches = dataLoaderService.loadDataBatch(content);
for (IncomingBatch batch : batches) {
if (batch.getStatus() == Status.ER) {
inError = true;
}
}
} catch (Exception e) {
inError = true;
}
if (inError) {
throw new InternalServerErrorException();
}
}
private NodeList childrenImpl(ISymmetricEngine engine) {
NodeList children = new NodeList();
Node xmlChildNode = null;
INodeService nodeService = engine.getNodeService();
org.jumpmind.symmetric.model.Node modelNode = nodeService.findIdentity();
if (isRegistered(engine)) {
if (isRootNode(engine, modelNode)) {
NetworkedNode networkedNode = nodeService.getRootNetworkedNode();
Set<NetworkedNode> childNetwork = networkedNode.getChildren();
if (childNetwork != null) {
for (NetworkedNode child : childNetwork) {
List<NodeHost> nodeHosts = nodeService.findNodeHosts(child.getNode()
.getNodeId());
NodeSecurity nodeSecurity = nodeService.findNodeSecurity(child.getNode()
.getNodeId());
xmlChildNode = new Node();
xmlChildNode.setNodeId(child.getNode().getNodeId());
xmlChildNode.setExternalId(child.getNode().getExternalId());
xmlChildNode.setRegistrationServer(false);
xmlChildNode.setSyncUrl(child.getNode().getSyncUrl());
xmlChildNode.setBatchInErrorCount(child.getNode().getBatchInErrorCount());
xmlChildNode.setBatchToSendCount(child.getNode().getBatchToSendCount());
if (nodeHosts.size() > 0) {
xmlChildNode.setLastHeartbeat(nodeHosts.get(0).getHeartbeatTime());
}
xmlChildNode.setRegistered(nodeSecurity.hasRegistered());
xmlChildNode.setInitialLoaded(nodeSecurity.hasInitialLoaded());
xmlChildNode
.setReverseInitialLoaded(nodeSecurity.hasReverseInitialLoaded());
if (child.getNode().getCreatedAtNodeId() == null) {
xmlChildNode.setRegistrationServer(true);
}
children.addNode(xmlChildNode);
}
}
}
} else {
throw new NotFoundException();
}
return children;
}
private Node nodeImpl(ISymmetricEngine engine) {
Node xmlNode = new Node();
if (isRegistered(engine)) {
INodeService nodeService = engine.getNodeService();
org.jumpmind.symmetric.model.Node modelNode = nodeService.findIdentity(false);
List<NodeHost> nodeHosts = nodeService.findNodeHosts(modelNode.getNodeId());
NodeSecurity nodeSecurity = nodeService.findNodeSecurity(modelNode.getNodeId());
xmlNode.setNodeId(modelNode.getNodeId());
xmlNode.setExternalId(modelNode.getExternalId());
xmlNode.setSyncUrl(modelNode.getSyncUrl());
xmlNode.setRegistrationUrl(engine.getParameterService().getRegistrationUrl());
xmlNode.setBatchInErrorCount(modelNode.getBatchInErrorCount());
xmlNode.setBatchToSendCount(modelNode.getBatchToSendCount());
if (nodeHosts.size() > 0) {
xmlNode.setLastHeartbeat(nodeHosts.get(0).getHeartbeatTime());
}
xmlNode.setHeartbeatInterval(engine.getParameterService().getInt(
ParameterConstants.HEARTBEAT_JOB_PERIOD_MS));
xmlNode.setRegistered(nodeSecurity.hasRegistered());
xmlNode.setInitialLoaded(nodeSecurity.hasInitialLoaded());
xmlNode.setReverseInitialLoaded(nodeSecurity.hasReverseInitialLoaded());
if (modelNode.getCreatedAtNodeId() == null) {
xmlNode.setRegistrationServer(true);
} else {
xmlNode.setRegistrationServer(false);
}
xmlNode.setCreatedAtNodeId(modelNode.getCreatedAtNodeId());
} else {
throw new NotFoundException();
}
return xmlNode;
}
private boolean isRootNode(ISymmetricEngine engine, org.jumpmind.symmetric.model.Node node) {
INodeService nodeService = engine.getNodeService();
org.jumpmind.symmetric.model.Node modelNode = nodeService.findIdentity();
if (modelNode.getCreatedAtNodeId() == null
|| modelNode.getCreatedAtNodeId().equalsIgnoreCase(modelNode.getExternalId())) {
return true;
} else {
return false;
}
}
private boolean isRegistered(ISymmetricEngine engine) {
boolean registered = true;
INodeService nodeService = engine.getNodeService();
org.jumpmind.symmetric.model.Node modelNode = nodeService.findIdentity(false);
if (modelNode == null) {
registered = false;
} else {
NodeSecurity nodeSecurity = nodeService.findNodeSecurity(modelNode.getNodeId());
if (nodeSecurity == null) {
registered = false;
}
}
return registered;
}
private NodeStatus nodeStatusImpl(ISymmetricEngine engine) {
NodeStatus status = new NodeStatus();
if (isRegistered(engine)) {
INodeService nodeService = engine.getNodeService();
org.jumpmind.symmetric.model.Node modelNode = nodeService.findIdentity(false);
NodeSecurity nodeSecurity = nodeService.findNodeSecurity(modelNode.getNodeId());
List<NodeHost> nodeHost = nodeService.findNodeHosts(modelNode.getNodeId());
status.setStarted(engine.isStarted());
status.setRegistered(nodeSecurity.getRegistrationTime() != null);
status.setInitialLoaded(nodeSecurity.getInitialLoadTime() != null);
status.setReverseInitialLoaded(nodeSecurity.getRevInitialLoadTime() != null);
status.setNodeId(modelNode.getNodeId());
status.setNodeGroupId(modelNode.getNodeGroupId());
status.setExternalId(modelNode.getExternalId());
status.setSyncUrl(modelNode.getSyncUrl());
status.setRegistrationUrl(engine.getParameterService().getRegistrationUrl());
status.setDatabaseType(modelNode.getDatabaseType());
status.setDatabaseVersion(modelNode.getDatabaseVersion());
status.setSyncEnabled(modelNode.isSyncEnabled());
status.setCreatedAtNodeId(modelNode.getCreatedAtNodeId());
status.setBatchToSendCount(engine.getOutgoingBatchService()
.countOutgoingBatchesUnsent());
status.setBatchInErrorCount(engine.getOutgoingBatchService()
.countOutgoingBatchesInError());
status.setDeploymentType(modelNode.getDeploymentType());
if (modelNode.getCreatedAtNodeId() == null) {
status.setRegistrationServer(true);
} else {
status.setRegistrationServer(false);
}
if (nodeHost != null && nodeHost.size() > 0) {
status.setLastHeartbeat(nodeHost.get(0).getHeartbeatTime());
}
status.setHeartbeatInterval(engine.getParameterService().getInt(
ParameterConstants.HEARTBEAT_SYNC_ON_PUSH_PERIOD_SEC));
if (status.getHeartbeatInterval() == 0) {
status.setHeartbeatInterval(600);
}
} else {
throw new NotFoundException();
}
return status;
}
private Set<ChannelStatus> channelStatusImpl(ISymmetricEngine engine) {
HashSet<ChannelStatus> channelStatus = new HashSet<ChannelStatus>();
List<NodeChannel> channels = engine.getConfigurationService().getNodeChannels(false);
for (NodeChannel nodeChannel : channels) {
String channelId = nodeChannel.getChannelId();
ChannelStatus status = new ChannelStatus();
status.setChannelId(channelId);
int outgoingInError = engine.getOutgoingBatchService().countOutgoingBatchesInError(
channelId);
int incomingInError = engine.getIncomingBatchService().countIncomingBatchesInError(
channelId);
status.setBatchInErrorCount(outgoingInError);
status.setBatchToSendCount(engine.getOutgoingBatchService().countOutgoingBatchesUnsent(
channelId));
status.setIncomingError(incomingInError > 0);
status.setOutgoingError(outgoingInError > 0);
status.setEnabled(nodeChannel.isEnabled());
status.setIgnoreEnabled(nodeChannel.isIgnoreEnabled());
status.setSuspendEnabled(nodeChannel.isSuspendEnabled());
channelStatus.add(status);
}
return channelStatus;
}
private QueryResults queryNodeImpl(ISymmetricEngine engine, String sql) {
QueryResults results = new QueryResults();
org.jumpmind.symmetric.web.rest.model.Row xmlRow = null;
org.jumpmind.symmetric.web.rest.model.Column xmlColumn = null;
ISqlTemplate sqlTemplate = engine.getSqlTemplate();
try {
List<Row> rows = sqlTemplate.query(sql);
int nbrRows = 0;
for (Row row : rows) {
xmlRow = new org.jumpmind.symmetric.web.rest.model.Row();
Iterator<Map.Entry<String, Object>> itr = row.entrySet().iterator();
int columnOrdinal = 0;
while (itr.hasNext()) {
xmlColumn = new org.jumpmind.symmetric.web.rest.model.Column();
xmlColumn.setOrdinal(++columnOrdinal);
Map.Entry<String, Object> pair = (Map.Entry<String, Object>) itr.next();
xmlColumn.setName(pair.getKey());
if (pair.getValue() != null) {
xmlColumn.setValue(pair.getValue().toString());
}
xmlRow.getColumnData().add(xmlColumn);
}
xmlRow.setRowNum(++nbrRows);
results.getResults().add(xmlRow);
}
results.setNbrResults(nbrRows);
} catch (Exception ex) {
log.error("Exception while executing sql.", ex);
throw new NotAllowedException("Error while executing sql %s. Error is %s", sql, ex
.getCause().getMessage());
}
return results;
}
protected SymmetricEngineHolder getSymmetricEngineHolder() {
SymmetricEngineHolder holder = (SymmetricEngineHolder) context
.getAttribute(WebConstants.ATTR_ENGINE_HOLDER);
if (holder == null) {
throw new NotFoundException();
}
return holder;
}
protected ISymmetricEngine getSymmetricEngine(String engineName) {
SymmetricEngineHolder holder = getSymmetricEngineHolder();
ISymmetricEngine engine = null;
if (StringUtils.isNotBlank(engineName)) {
engine = holder.getEngines().get(engineName);
}
if (engine == null) {
throw new NotFoundException();
} else if (!engine.getParameterService().is(ParameterConstants.REST_API_ENABLED)) {
throw new NotAllowedException("The REST API was not enabled for %s",
engine.getEngineName());
} else {
MDC.put("engineName", engine.getEngineName());
return engine;
}
}
protected boolean securityVerified(String nodeId, ISymmetricEngine engine, String securityToken) {
INodeService nodeService = engine.getNodeService();
boolean allowed = false;
org.jumpmind.symmetric.model.Node targetNode = nodeService.findNode(nodeId);
if (targetNode != null) {
NodeSecurity security = nodeService.findNodeSecurity(nodeId);
allowed = security.getNodePassword().equals(securityToken);
}
return allowed;
}
protected ISymmetricEngine getSymmetricEngine() {
ISymmetricEngine engine = null;
SymmetricEngineHolder holder = getSymmetricEngineHolder();
if (holder.getEngines().size() > 0) {
engine = holder.getEngines().values().iterator().next();
}
if (engine == null) {
throw new NotAllowedException();
} else if (!engine.getParameterService().is(ParameterConstants.REST_API_ENABLED)) {
throw new NotAllowedException("The REST API was not enabled for %s",
engine.getEngineName());
} else {
return engine;
}
}
}