/** * 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; } } }