001: /**
002: * Sequoia: Database clustering technology.
003: * Copyright (C) 2006 Continuent Inc.
004: * Contact: sequoia@continuent.org
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: *
018: * Initial developer(s): Adam Fletcher, Mykola Paliyenko.
019: * Contributor(s): Emmanuel Cecchet, Stephane Giron
020: */package org.continuent.sequoia.controller.backup.backupers;
021:
022: import java.io.File;
023: import java.io.IOException;
024: import java.io.InputStream;
025: import java.sql.Connection;
026: import java.sql.DriverManager;
027: import java.sql.Statement;
028: import java.util.ArrayList;
029: import java.util.Date;
030: import java.util.HashMap;
031: import java.util.Iterator;
032: import java.util.StringTokenizer;
033: import java.util.regex.Matcher;
034: import java.util.regex.Pattern;
035:
036: import org.continuent.sequoia.common.exceptions.BackupException;
037: import org.continuent.sequoia.common.log.Trace;
038: import org.continuent.sequoia.controller.backend.DatabaseBackend;
039: import org.continuent.sequoia.controller.backup.BackupManager;
040: import org.continuent.sequoia.controller.backup.Backuper;
041: import org.continuent.sequoia.controller.backup.DumpTransferInfo;
042:
043: /**
044: * MSSQL backuper inspired from the PostgreSQL backuper. <br>
045: * This backuper takes the following options: <br>
046: * urlHeader: expected URL header for the backend. Default is
047: * "jdbc:jtds:sqlserver:" <br>
048: * driverClassName: driver class name to load. Default is the driver class
049: * defined for the targeted backend. If you need to use a specific driver for
050: * backup/restore operation, you can force it here.
051: * <p>
052: * To use it, edit the virtual database config's <Backup> node:
053: *
054: * <pre>
055: * <Backup>
056: * <Backuper backuperName="MSSQLServer" className="org.continuent.sequoia.controller.backup.backupers.MSSQLBackuper" options=""/>
057: * <Backuper backuperName="MSSQLServerWithjTDSDriver" className="org.continuent.sequoia.controller.backup.backupers.MSSQLBackuper" options="urlHeader=jdbc:jtds:sqlserver:,driverClassName=net.sourceforge.jtds.jdbc.Driver"/>
058: * <Backuper backuperName="MSSQLServerWithMSDriver" className="org.continuent.sequoia.controller.backup.backupers.MSSQLBackuper" options="urlHeader=jdbc:microsoft:sqlserver:,driverClassName=com.microsoft.jdbc.sqlserver.SQLServerDriver"/>
059: * </Backup>
060: *</pre>
061: *
062: * Then in the console to take the backups:<br>
063: * backup mybackend database.dump MSSQLServer \\<fileserver>\<share>
064: *
065: * @author <a href="mailto:adamf@powersteeringsoftware.com">Adam Fletcher</a>
066: * @author <a href="mailto:mpaliyenko@gmail.com">Mykola Paliyenko</a>
067: * @author <a href="mailto:emmanuel.cecchet@continuent.com">Emmanuel Cecchet</a>
068: * @author <a href="mailto:stephane.giron@continuent.com">Stephane Giron</a>
069: */
070: public class MSSQLBackuper implements Backuper {
071: protected HashMap optionsMap = new HashMap();
072: protected String optionsString = null;
073:
074: private static final String DEFAULT_MSSQL_PORT = "1433";
075: private static final String DEFAULT_MSSQL_HOST = "localhost";
076: private static final String DEFAULT_JDBC_URL = "jdbc:jtds:sqlserver:";
077:
078: private static final Object URL_OPTION = "urlHeader";
079: private static final Object DRIVER_CLASS_NAME_OPTION = "driverClassName";
080:
081: static Trace logger = Trace
082: .getLogger(MSSQLBackuper.class.getName());
083:
084: private ArrayList errors;
085:
086: /**
087: * Creates a new <code>MSSQLBackuper</code> object
088: */
089: public MSSQLBackuper() {
090: errors = new ArrayList();
091: }
092:
093: /**
094: * @see org.continuent.sequoia.controller.backup.Backuper#getDumpFormat()
095: */
096: public String getDumpFormat() {
097: return "MSSQL raw dump";
098: }
099:
100: /**
101: * @see Backuper#getOptions()
102: */
103: public String getOptions() {
104: return optionsString;
105: }
106:
107: /**
108: * @see Backuper#setOptions(java.lang.String)
109: */
110: public void setOptions(String options) {
111: if (options != null) {
112: StringTokenizer strTok = new StringTokenizer(options, ",");
113: String option = null;
114: String name = null;
115: String value = null;
116:
117: // Parse the string of options, add them to the HashMap
118: while (strTok.hasMoreTokens()) {
119: option = strTok.nextToken();
120: name = option.substring(0, option.indexOf("="));
121: value = option.substring(option.indexOf("=") + 1,
122: option.length());
123: optionsMap.put(name, value);
124: }
125:
126: optionsString = options;
127: }
128: }
129:
130: /**
131: * @see org.continuent.sequoia.controller.backup.Backuper#backup(org.continuent.sequoia.controller.backend.DatabaseBackend,
132: * java.lang.String, java.lang.String, java.lang.String,
133: * java.lang.String, java.util.ArrayList)
134: */
135: public Date backup(DatabaseBackend backend, String login,
136: String password, String dumpName, String path,
137: ArrayList tables) throws BackupException {
138: String url = backend.getURL();
139:
140: String expectedUrl = (String) optionsMap.get(URL_OPTION);
141: if (expectedUrl == null)
142: expectedUrl = DEFAULT_JDBC_URL;
143:
144: if (!url.startsWith(expectedUrl)) {
145: throw new BackupException("Unsupported db url " + url);
146: }
147: MSSQLUrlInfo info = new MSSQLUrlInfo(url);
148: Connection con;
149:
150: try {
151: String driverClassName = (String) optionsMap
152: .get(DRIVER_CLASS_NAME_OPTION);
153: if (driverClassName == null)
154: driverClassName = backend.getDriverClassName();
155:
156: // Load the driver and connect to the database
157: Class.forName(driverClassName);
158: con = DriverManager.getConnection(url + ";user=" + login
159: + ";password=" + password);
160: } catch (Exception e) {
161: String msg = "Error while performing backup during creation of connection";
162: logger.error(msg, e);
163: throw new BackupException(msg, e);
164: }
165:
166: try {
167: File pathDir = new File(path);
168: if (!pathDir.exists()) {
169: pathDir.mkdirs();
170: pathDir.mkdir();
171: }
172:
173: /*
174: * What should be done here: 1) verify the path is a UNC path 2) connect
175: * to the server via JDBC 3) issue the 'BACKUP <dbname> TO DISK='<path>'
176: * <path> must be UNC connect to the configured backend with JDBC
177: */
178: // String dumpPath = path + File.separator + dumpName;
179: // we don't use File.seperator here because we are using UNC paths
180: String dumpPath = path + "\\" + dumpName;
181:
182: if (logger.isDebugEnabled()) {
183: logger.debug("Dumping " + backend.getURL() + " in "
184: + dumpPath);
185: }
186:
187: Statement stmt = con.createStatement();
188: String sqlStatement = "BACKUP DATABASE " + info.getDbName()
189: + " TO DISK = '" + dumpPath + "'";
190:
191: logger.debug("sql statement for backup: " + sqlStatement);
192:
193: boolean backupResult = stmt.execute(sqlStatement);
194:
195: if (backupResult) {
196: String msg = "BACKUP returned false";
197: logger.error(msg);
198: throw new BackupException(msg);
199: }
200: } catch (Exception e) {
201: String msg = "Error while performing backup";
202: logger.error(msg, e);
203: throw new BackupException(msg, e);
204: } finally {
205: try {
206: con.close();
207: } catch (Exception e) {
208: String msg = "Error while performing backup during close connection";
209: logger.error(msg, e);
210: throw new BackupException(msg, e);
211: }
212: }
213: return new Date();
214: }
215:
216: /**
217: * @see org.continuent.sequoia.controller.backup.Backuper#restore(org.continuent.sequoia.controller.backend.DatabaseBackend,
218: * java.lang.String, java.lang.String, java.lang.String,
219: * java.lang.String, java.util.ArrayList)
220: */
221: public void restore(DatabaseBackend backend, String login,
222: String password, String dumpName, String path,
223: ArrayList tables) throws BackupException {
224: String url = backend.getURL();
225:
226: String expectedUrl = (String) optionsMap.get(URL_OPTION);
227:
228: if (expectedUrl == null)
229: expectedUrl = DEFAULT_JDBC_URL;
230:
231: if (!url.startsWith(expectedUrl)) {
232: throw new BackupException("Unsupported db url " + url);
233: }
234:
235: MSSQLUrlInfo info = new MSSQLUrlInfo(url);
236: Connection con;
237: try {
238: String driverClassName = (String) optionsMap
239: .get(DRIVER_CLASS_NAME_OPTION);
240: if (driverClassName == null)
241: driverClassName = backend.getDriverClassName();
242:
243: // Load the driver and connect to the database
244: Class.forName(driverClassName);
245:
246: // NOTE: you have to connect to the master database to restore,
247: // because if you connect to the current DB you are USING the db
248: // and therefore LOCKING the db.
249:
250: con = DriverManager
251: .getConnection(expectedUrl + "//" + info.getHost()
252: + ":" + info.getPort() + "/master;user="
253: + login + ";password=" + password);
254: } catch (Exception e) {
255: String msg = "Error while performing restore during creation of connection";
256: logger.error(msg, e);
257: throw new BackupException(msg, e);
258: }
259:
260: try {
261: File pathDir = new File(path);
262: if (!pathDir.exists()) {
263: pathDir.mkdirs();
264: pathDir.mkdir();
265: }
266: String dumpPath = path + File.separator + dumpName;
267: if (logger.isDebugEnabled()) {
268: logger.debug("Restoring " + backend.getURL() + " from "
269: + dumpPath);
270: }
271:
272: Statement stmt = con.createStatement();
273:
274: String sqlAtatement = "RESTORE DATABASE "
275: + info.getDbName() + " FROM DISK = '" + dumpPath
276: + "'";
277:
278: boolean backupResult = stmt.execute(sqlAtatement);
279:
280: if (backupResult) {
281: String msg = "restore returned false";
282: logger.error(msg);
283: throw new BackupException(msg);
284: }
285: } catch (Exception e) {
286: String msg = "Error while performing restore";
287: logger.error(msg, e);
288: throw new BackupException(msg, e);
289: } finally {
290: try {
291: con.close();
292: } catch (Exception e) {
293: String msg = "Error while performing restore during close connection";
294: logger.error(msg, e);
295: throw new BackupException(msg, e);
296: }
297: }
298: }
299:
300: String getProcessOutput(Process process) throws IOException {
301: StringBuffer sb = new StringBuffer();
302:
303: InputStream pis = process.getInputStream();
304: InputStream pes = process.getErrorStream();
305:
306: int c = pis.read();
307: while (c != -1) {
308: sb.append(new Character((char) c));
309: c = pis.read();
310: }
311: c = pes.read();
312: while (c != -1) {
313: sb.append(new Character((char) c));
314: c = pes.read();
315: }
316:
317: return sb.toString();
318: }
319:
320: /**
321: * @see org.continuent.sequoia.controller.backup.Backuper#deleteDump(java.lang.String,
322: * java.lang.String)
323: */
324: public void deleteDump(String path, String dumpName)
325: throws BackupException {
326: File toRemove = new File(getDumpPhysicalPath(path, dumpName));
327: if (logger.isDebugEnabled())
328: logger.debug("Deleting compressed dump " + toRemove);
329: toRemove.delete();
330: }
331:
332: /**
333: * Get the dump physical path from its logical name
334: *
335: * @param path the path where the dump is stored
336: * @param dumpName dump logical name
337: * @return path to zip file
338: */
339: private String getDumpPhysicalPath(String path, String dumpName) {
340: return path + File.separator + dumpName;
341: }
342:
343: /**
344: * @see org.continuent.sequoia.controller.backup.Backuper#fetchDump(org.continuent.sequoia.controller.backup.DumpTransferInfo,
345: * java.lang.String, java.lang.String)
346: */
347: public void fetchDump(DumpTransferInfo dumpTransferInfo,
348: String path, String dumpName) throws BackupException,
349: IOException {
350: BackupManager.fetchDumpFile(dumpTransferInfo, path, dumpName);
351: }
352:
353: /**
354: * @see org.continuent.sequoia.controller.backup.Backuper#setupDumpServer()
355: */
356: public DumpTransferInfo setupDumpServer() throws IOException {
357: return BackupManager.setupDumpFileServer();
358: }
359:
360: /**
361: * Allow to parse PostgreSQL URL.
362: */
363: protected class MSSQLUrlInfo {
364: private boolean isLocal;
365:
366: private String host;
367:
368: private String port;
369:
370: private String dbName;
371:
372: /**
373: * Creates a new <code>MSSQLUrlInfo</code> object, used to parse the MSSQL
374: * JDBC options. If host and/or port aren't specified, will default to
375: * localhost:1433. Note that database name must be specified.
376: *
377: * @param url the MSSQL JDBC url to parse
378: */
379: public MSSQLUrlInfo(String url) {
380: String expectedUrl = (String) optionsMap.get(URL_OPTION);
381:
382: if (expectedUrl == null)
383: expectedUrl = DEFAULT_JDBC_URL;
384:
385: // Used to parse url
386: Pattern pattern = Pattern
387: .compile(expectedUrl
388: + "((//([a-zA-Z0-9_\\-.]+|\\[[a-fA-F0-9:]+])((:(\\d+))|))/|)([a-zA-Z][a-zA-Z0-9_\\-]*)(\\?.*)?");
389:
390: Matcher matcher;
391:
392: matcher = pattern.matcher(url);
393:
394: if (matcher.matches()) {
395: if (matcher.group(3) != null)
396: host = matcher.group(3);
397: else
398: host = DEFAULT_MSSQL_HOST;
399:
400: if (matcher.group(6) != null)
401: port = matcher.group(6);
402: else
403: port = DEFAULT_MSSQL_PORT;
404:
405: dbName = matcher.group(7);
406: }
407: }
408:
409: /**
410: * Gets the HostParameters of this postgresql jdbc url as a String that can
411: * be used to pass into cmd line/shell calls.
412: *
413: * @return a string that can be used to pass into a cmd line/shell call.
414: */
415: public String getHostParametersString() {
416: logger.debug("getHostParamertsString host: " + host
417: + " port: " + port);
418:
419: if (isLocal) {
420: return "//localhost";
421: } else {
422: return "//" + host + ":" + port;
423: }
424: }
425:
426: /**
427: * Gets the database name part of this postgresql jdbc url.
428: *
429: * @return the database name part of this postgresql jdbc url.
430: */
431: public String getDbName() {
432: return dbName;
433: }
434:
435: /**
436: * Gets the host part of this postgresql jdbc url.
437: *
438: * @return the host part of this postgresql jdbc url.
439: */
440: public String getHost() {
441: return host;
442: }
443:
444: /**
445: * Gets the port part of this postgresql jdbc url.
446: *
447: * @return the port part of this postgresql jdbc url.
448: */
449: public String getPort() {
450: return port;
451: }
452:
453: /**
454: * Checks whether this postgresql jdbc url refers to a local db or not, i.e.
455: * has no host specified, e.g. jdbc:postgresql:myDb.
456: *
457: * @return true if this postgresql jdbc url has no host specified, i.e.
458: * refers to a local db.
459: */
460: public boolean isLocal() {
461: return isLocal;
462: }
463:
464: }
465:
466: protected void printErrors() {
467: Iterator it = errors.iterator();
468: while (it.hasNext()) {
469: logger.info(it.next());
470: }
471: }
472: }
|