// // Copyright (c) 2009 Microsoft Corporation. All rights reserved. // // DISCLAIMER OF WARRANTY: The software is licensed “as-is.” You // bear the risk of using it. Microsoft gives no express warranties, // guarantees or conditions. You may have additional consumer rights // under your local laws which this agreement cannot change. To the extent // permitted under your local laws, Microsoft excludes the implied warranties // of merchantability, fitness for a particular purpose and non-infringement. using System; using System.IO; using System.Data; using System.Data.Odbc; using System.Diagnostics; using System.Collections; using System.Collections.ObjectModel; using System.Management.Automation; using System.Management.Automation.Provider; using System.Text; using System.Text.RegularExpressions; using System.ComponentModel; using System.Globalization; namespace Microsoft.Samples.PowerShell.Providers { #region AccessDBProvider /// /// This example implements the content methods. /// [CmdletProvider("AccessDB", ProviderCapabilities.None)] public class AccessDBProvider : NavigationCmdletProvider, IContentCmdletProvider { #region Drive Manipulation /// /// Create a new drive. Create a connection to the database file and set /// the Connection property in the PSDriveInfo. /// /// /// Information describing the drive to add. /// /// The added drive. protected override PSDriveInfo NewDrive(PSDriveInfo drive) { // check if drive object is null if (drive == null) { WriteError(new ErrorRecord( new ArgumentNullException("drive"), "NullDrive", ErrorCategory.InvalidArgument, null) ); return null; } // check if drive root is not null or empty // and if its an existing file if (String.IsNullOrEmpty(drive.Root) || (File.Exists(drive.Root) == false)) { WriteError(new ErrorRecord( new ArgumentException("drive.Root"), "NoRoot", ErrorCategory.InvalidArgument, drive) ); return null; } // create a new drive and create an ODBC connection to the new drive AccessDBPSDriveInfo accessDBPSDriveInfo = new AccessDBPSDriveInfo(drive); OdbcConnectionStringBuilder builder = new OdbcConnectionStringBuilder(); builder.Driver = "Microsoft Access Driver (*.mdb)"; builder.Add("DBQ", drive.Root); OdbcConnection conn = new OdbcConnection(builder.ConnectionString); conn.Open(); accessDBPSDriveInfo.Connection = conn; return accessDBPSDriveInfo; } // NewDrive /// /// Removes a drive from the provider. /// /// The drive to remove. /// The drive removed. protected override PSDriveInfo RemoveDrive(PSDriveInfo drive) { // check if drive object is null if (drive == null) { WriteError(new ErrorRecord( new ArgumentNullException("drive"), "NullDrive", ErrorCategory.InvalidArgument, drive) ); return null; } // close ODBC connection to the drive AccessDBPSDriveInfo accessDBPSDriveInfo = drive as AccessDBPSDriveInfo; if (accessDBPSDriveInfo == null) { return null; } accessDBPSDriveInfo.Connection.Close(); return accessDBPSDriveInfo; } // RemoveDrive #endregion Drive Manipulation #region Item Methods /// /// Retrieves an item using the specified path. /// /// The path to the item to return. protected override void GetItem(string path) { // check if the path represented is a drive if (PathIsDrive(path)) { WriteItemObject(this.PSDriveInfo, path, true); return; }// if (PathIsDrive... // Get table name and row information from the path and do // necessary actions string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Table) { DatabaseTableInfo table = GetTable(tableName); WriteItemObject(table, path, true); } else if (type == PathType.Row) { DatabaseRowInfo row = GetRow(tableName, rowNumber); WriteItemObject(row, path, false); } else { ThrowTerminatingInvalidPathException(path); } } // GetItem /// /// Set the content of a row of data specified by the supplied path /// parameter. /// /// Specifies the path to the row whose columns /// will be updated. /// Comma separated string of values protected override void SetItem(string path, object values) { // Get type, table name and row number from the path specified string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type != PathType.Row) { WriteError(new ErrorRecord(new NotSupportedException( "SetNotSupported"), "", ErrorCategory.InvalidOperation, path)); return; } // Get in-memory representation of table OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); if (rowNumber >= table.Rows.Count) { // The specified row number has to be available. If not // NewItem has to be used to add a new row throw new ArgumentException("Row specified is not available"); } // if (rowNum... string[] colValues = (values as string).Split(','); // set the specified row DataRow row = table.Rows[rowNumber]; for (int i = 0; i < colValues.Length; i++) { row[i] = colValues[i]; } // Update the table if (ShouldProcess(path, "SetItem")) { da.Update(ds, tableName); } } // SetItem /// /// Test to see if the specified item exists. /// /// The path to the item to verify. /// True if the item is found. protected override bool ItemExists(string path) { // check if the path represented is a drive if (PathIsDrive(path)) { return true; } // Obtain type, table name and row number from path string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); DatabaseTableInfo table = GetTable(tableName); if (type == PathType.Table) { // if specified path represents a table then DatabaseTableInfo // object for the same should exist if (table != null) { return true; } } else if (type == PathType.Row) { // if specified path represents a row then DatabaseTableInfo should // exist for the table and then specified row number must be within // the maximum row count in the table if (table != null && rowNumber < table.RowCount) { return true; } } return false; } // ItemExists /// /// Test to see if the specified path is syntactically valid. /// /// The path to validate. /// True if the specified path is valid. protected override bool IsValidPath(string path) { bool result = true; // check if the path is null or empty if (String.IsNullOrEmpty(path)) { result = false; } // convert all separators in the path to a uniform one path = NormalizePath(path); // split the path into individual chunks string[] pathChunks = path.Split(pathSeparator.ToCharArray()); foreach (string pathChunk in pathChunks) { if (pathChunk.Length == 0) { result = false; } } return result; } // IsValidPath #endregion Item Overloads #region Container Overloads /// /// Return either the tables in the database or the datarows /// /// The path to the parent /// True to return all child items recursively. /// protected override void GetChildItems(string path, bool recurse) { // If path represented is a drive then the children in the path are // tables. Hence all tables in the drive represented will have to be // returned if (PathIsDrive(path)) { foreach (DatabaseTableInfo table in GetTables()) { WriteItemObject(table, path, true); // if the specified item exists and recurse has been set then // all child items within it have to be obtained as well if (ItemExists(path) && recurse) { GetChildItems(path + pathSeparator + table.Name, recurse); } } // foreach (DatabaseTableInfo... } // if (PathIsDrive... else { // Get the table name, row number and type of path from the // path specified string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Table) { // Obtain all the rows within the table foreach (DatabaseRowInfo row in GetRows(tableName)) { WriteItemObject(row, path + pathSeparator + row.RowNumber, false); } // foreach (DatabaseRowInfo... } else if (type == PathType.Row) { // In this case the user has directly specified a row, hence // just give that particular row DatabaseRowInfo row = GetRow(tableName, rowNumber); WriteItemObject(row, path + pathSeparator + row.RowNumber, false); } else { // In this case, the path specified is not valid ThrowTerminatingInvalidPathException(path); } } // else } // GetChildItems /// /// Return the names of all child items. /// /// The root path. /// Not used. protected override void GetChildNames(string path, ReturnContainers returnContainers) { // If the path represented is a drive, then the child items are // tables. get the names of all the tables in the drive. if (PathIsDrive(path)) { foreach (DatabaseTableInfo table in GetTables()) { WriteItemObject(table.Name, path, true); } // foreach (DatabaseTableInfo... } // if (PathIsDrive... else { // Get type, table name and row number from path specified string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Table) { // Get all the rows in the table and then write out the // row numbers. foreach (DatabaseRowInfo row in GetRows(tableName)) { WriteItemObject(row.RowNumber, path, false); } // foreach (DatabaseRowInfo... } else if (type == PathType.Row) { // In this case the user has directly specified a row, hence // just give that particular row DatabaseRowInfo row = GetRow(tableName, rowNumber); WriteItemObject(row.RowNumber, path, false); } else { ThrowTerminatingInvalidPathException(path); } } // else } // GetChildNames /// /// Determines if the specified path has child items. /// /// The path to examine. /// /// True if the specified path has child items. /// protected override bool HasChildItems(string path) { if (PathIsDrive(path)) { return true; } return (ChunkPath(path).Length == 1); } // HasChildItems /// /// Creates a new item at the specified path. /// /// /// /// The path to the new item. /// /// /// /// Type for the object to create. "Table" for creating a new table and /// "Row" for creating a new row in a table. /// /// /// /// Object for creating new instance of a type at the specified path. For /// creating a "Table" the object parameter is ignored and for creating /// a "Row" the object must be of type string which will contain comma /// separated values of the rows to insert. /// protected override void NewItem(string path, string type, object newItemValue) { string tableName; int rowNumber; PathType pt = GetNamesFromPath(path, out tableName, out rowNumber); if (pt == PathType.Invalid) { ThrowTerminatingInvalidPathException(path); } // Check if type is either "table" or "row", if not throw an // exception if (!String.Equals(type, "table", StringComparison.OrdinalIgnoreCase) && !String.Equals(type, "row", StringComparison.OrdinalIgnoreCase)) { WriteError(new ErrorRecord (new ArgumentException("Type must be either a table or row"), "CannotCreateSpecifiedObject", ErrorCategory.InvalidArgument, path ) ); throw new ArgumentException("This provider can only create items of type \"table\" or \"row\""); } // Path type is the type of path of the container. So if a drive // is specified, then a table can be created under it and if a table // is specified, then a row can be created under it. For the sake of // completeness, if a row is specified, then if the row specified by // the path does not exist, a new row is created. However, the row // number may not match as the row numbers only get incremented based // on the number of rows if (PathIsDrive(path)) { if (String.Equals(type, "table", StringComparison.OrdinalIgnoreCase)) { // Execute command using ODBC connection to create a table try { // create the table using an sql statement string newTableName = newItemValue.ToString(); string sql = "create table " + newTableName + " (ID INT)"; // Create the table using the Odbc connection from the // drive. AccessDBPSDriveInfo di = this.PSDriveInfo as AccessDBPSDriveInfo; if (di == null) { return; } OdbcConnection connection = di.Connection; if (ShouldProcess(newTableName, "create")) { OdbcCommand cmd = new OdbcCommand(sql, connection); cmd.ExecuteScalar(); } } catch (Exception ex) { WriteError(new ErrorRecord(ex, "CannotCreateSpecifiedTable", ErrorCategory.InvalidOperation, path) ); } } // if (String... else if (String.Equals(type, "row", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("A row cannot be created under a database, specify a path that represents a Table"); } }// if (PathIsDrive... else { if (String.Equals(type, "table", StringComparison.OrdinalIgnoreCase)) { if (rowNumber < 0) { throw new ArgumentException("A table cannot be created within another table, specify a path that represents a database"); } else { throw new ArgumentException("A table cannot be created inside a row, specify a path that represents a database"); } } //if (String.Equals.... // if path specified is a row, create a new row else if (String.Equals(type, "row", StringComparison.OrdinalIgnoreCase)) { // The user is required to specify the values to be inserted // into the table in a single string separated by commas string value = newItemValue as string; if (String.IsNullOrEmpty(value)) { throw new ArgumentException("Value argument must have comma separated values of each column in a row"); } string[] rowValues = value.Split(','); OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); if (rowValues.Length != table.Columns.Count) { string message = String.Format(CultureInfo.CurrentCulture, "The table has {0} columns and the value specified must have so many comma separated values", table.Columns.Count); throw new ArgumentException(message); } if (!Force && (rowNumber >= 0 && rowNumber < table.Rows.Count)) { string message = String.Format(CultureInfo.CurrentCulture, "The row {0} already exists. To create a new row specify row number as {1}, or specify path to a table, or use the -Force parameter", rowNumber, table.Rows.Count); throw new ArgumentException(message); } if (rowNumber > table.Rows.Count) { string message = String.Format(CultureInfo.CurrentCulture, "To create a new row specify row number as {0}, or specify path to a table", table.Rows.Count); throw new ArgumentException(message); } // Create a new row and update the row with the input // provided by the user DataRow row = table.NewRow(); for (int i = 0; i < rowValues.Length; i++) { row[i] = rowValues[i]; } table.Rows.Add(row); if (ShouldProcess(tableName, "update rows")) { // Update the table from memory back to the data source da.Update(ds, tableName); } }// else if (String... }// else ... } // NewItem /// /// Copies an item at the specified path to the location specified /// /// /// /// Path of the item to copy /// /// /// /// Path of the item to copy to /// /// /// /// Tells the provider to recurse subcontainers when copying /// /// protected override void CopyItem(string path, string copyPath, bool recurse) { string tableName, copyTableName; int rowNumber, copyRowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); PathType copyType = GetNamesFromPath(copyPath, out copyTableName, out copyRowNumber); if (type == PathType.Invalid) { ThrowTerminatingInvalidPathException(path); } if (type == PathType.Invalid) { ThrowTerminatingInvalidPathException(copyPath); } // Get the table and the table to copy to OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); OdbcDataAdapter cda = GetAdapterForTable(copyTableName); if (cda == null) { return; } DataSet cds = GetDataSetForTable(cda, copyTableName); DataTable copyTable = GetDataTable(cds, copyTableName); // if source represents a table if (type == PathType.Table) { // if copyPath does not represent a table if (copyType != PathType.Table) { ArgumentException e = new ArgumentException("Table can only be copied on to another table location"); WriteError(new ErrorRecord(e, "PathNotValid", ErrorCategory.InvalidArgument, copyPath)); throw e; } // if table already exists then force parameter should be set // to force a copy if (!Force && GetTable(copyTableName) != null) { throw new ArgumentException("Specified path already exists"); } for (int i = 0; i < table.Rows.Count; i++) { DataRow row = table.Rows[i]; DataRow copyRow = copyTable.NewRow(); copyRow.ItemArray = row.ItemArray; copyTable.Rows.Add(copyRow); } } // if (type == ... // if source represents a row else { if (copyType == PathType.Row) { if (!Force && (copyRowNumber < copyTable.Rows.Count)) { throw new ArgumentException("Specified path already exists."); } DataRow row = table.Rows[rowNumber]; DataRow copyRow = null; if (copyRowNumber < copyTable.Rows.Count) { // copy to an existing row copyRow = copyTable.Rows[copyRowNumber]; copyRow.ItemArray = row.ItemArray; copyRow[0] = GetNextID(copyTable); } else if (copyRowNumber == copyTable.Rows.Count) { // copy to the next row in the table that will // be created copyRow = copyTable.NewRow(); copyRow.ItemArray = row.ItemArray; copyRow[0] = GetNextID(copyTable); copyTable.Rows.Add(copyRow); } else { // attempting to copy to a nonexistent row or a row // that cannot be created now - throw an exception string message = String.Format(CultureInfo.CurrentCulture, "The item cannot be specified to the copied row. Specify row number as {0}, or specify a path to the table.", table.Rows.Count); throw new ArgumentException(message); } } else { // destination path specified represents a table, // create a new row and copy the item DataRow copyRow = copyTable.NewRow(); copyRow.ItemArray = table.Rows[rowNumber].ItemArray; copyRow[0] = GetNextID(copyTable); copyTable.Rows.Add(copyRow); } } if (ShouldProcess(copyTableName, "CopyItems")) { cda.Update(cds, copyTableName); } } //CopyItem /// /// Removes (deletes) the item at the specified path /// /// /// /// The path to the item to remove. /// /// /// /// True if all children in a subtree should be removed, false if only /// the item at the specified path should be removed. Is applicable /// only for container (table) items. Its ignored otherwise (even if /// specified). /// /// /// /// There are no elements in this store which are hidden from the user. /// Hence this method will not check for the presence of the Force /// parameter /// /// protected override void RemoveItem(string path, bool recurse) { string tableName; int rowNumber = 0; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Table) { // if recurse flag has been specified, delete all the rows as well if (recurse) { OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); for (int i = 0; i < table.Rows.Count; i++) { table.Rows[i].Delete(); } if (ShouldProcess(path, "RemoveItem")) { da.Update(ds, tableName); RemoveTable(tableName); } }//if (recurse... else { // Remove the table if (ShouldProcess(path, "RemoveItem")) { RemoveTable(tableName); } } } else if (type == PathType.Row) { OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); table.Rows[rowNumber].Delete(); if (ShouldProcess(path, "RemoveItem")) { da.Update(ds, tableName); } } else { ThrowTerminatingInvalidPathException(path); } } // RemoveItem #endregion Container Overloads #region Navigation /// /// Determine if the path specified is that of a container. /// /// The path to check. /// True if the path specifies a container. protected override bool IsItemContainer(string path) { if (PathIsDrive(path)) { return true; } string[] pathChunks = ChunkPath(path); string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Table) { foreach (DatabaseTableInfo ti in GetTables()) { if (string.Equals(ti.Name, tableName, StringComparison.OrdinalIgnoreCase)) { return true; } } // foreach (DatabaseTableInfo... } // if (pathChunks... return false; } // IsItemContainer /// /// Get the name of the leaf element in the specified path /// /// /// /// The full or partial provider specific path /// /// /// /// The leaf element in the path /// protected override string GetChildName(string path) { if (PathIsDrive(path)) { return path; } string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Table) { return tableName; } else if (type == PathType.Row) { return rowNumber.ToString(CultureInfo.CurrentCulture); } else { ThrowTerminatingInvalidPathException(path); } return null; } /// /// Removes the child segment of the path and returns the remaining /// parent portion /// /// /// /// A full or partial provider specific path. The path may be to an /// item that may or may not exist. /// /// /// /// The fully qualified path to the root of a drive. This parameter /// may be null or empty if a mounted drive is not in use for this /// operation. If this parameter is not null or empty the result /// of the method should not be a path to a container that is a /// parent or in a different tree than the root. /// /// /// protected override string GetParentPath(string path, string root) { // If root is specified then the path has to contain // the root. If not nothing should be returned if (!String.IsNullOrEmpty(root)) { if (!path.Contains(root)) { return null; } } return path.Substring(0, path.LastIndexOf(pathSeparator, StringComparison.OrdinalIgnoreCase)); } /// /// Joins two strings with a provider specific path separator. /// /// /// /// The parent segment of a path to be joined with the child. /// /// /// /// The child segment of a path to be joined with the parent. /// /// /// /// A string that represents the parent and child segments of the path /// joined by a path separator. /// protected override string MakePath(string parent, string child) { string result; string normalParent = NormalizePath(parent); normalParent = RemoveDriveFromPath(normalParent); string normalChild = NormalizePath(child); normalChild = RemoveDriveFromPath(normalChild); if (String.IsNullOrEmpty(normalParent) && String.IsNullOrEmpty(normalChild)) { result = String.Empty; } else if (String.IsNullOrEmpty(normalParent) && !String.IsNullOrEmpty(normalChild)) { result = normalChild; } else if (!String.IsNullOrEmpty(normalParent) && String.IsNullOrEmpty(normalChild)) { if (normalParent.EndsWith(pathSeparator, StringComparison.OrdinalIgnoreCase)) { result = normalParent; } else { result = normalParent + pathSeparator; } } // else if (!String... else { if (!normalParent.Equals(String.Empty, StringComparison.OrdinalIgnoreCase) && !normalParent.EndsWith(pathSeparator, StringComparison.OrdinalIgnoreCase)) { result = normalParent + pathSeparator; } else { result = normalParent; } if (normalChild.StartsWith(pathSeparator, StringComparison.OrdinalIgnoreCase)) { result += normalChild.Substring(1); } else { result += normalChild; } } // else return result; } // MakePath /// /// Normalizes the path that was passed in and returns the normalized /// path as a relative path to the basePath that was passed. /// /// /// /// A fully qualified provider specific path to an item. The item /// should exist or the provider should write out an error. /// /// /// /// The path that the return value should be relative to. /// /// /// /// A normalized path that is relative to the basePath that was /// passed. The provider should parse the path parameter, normalize /// the path, and then return the normalized path relative to the /// basePath. /// protected override string NormalizeRelativePath(string path, string basepath) { // Normalize the paths first string normalPath = NormalizePath(path); normalPath = RemoveDriveFromPath(normalPath); string normalBasePath = NormalizePath(basepath); normalBasePath = RemoveDriveFromPath(normalBasePath); if (String.IsNullOrEmpty(normalBasePath)) { return normalPath; } else { if (!normalPath.Contains(normalBasePath)) { return null; } return normalPath.Substring(normalBasePath.Length + pathSeparator.Length); } } /// /// Moves the item specified by the path to the specified destination /// /// /// /// The path to the item to be moved /// /// /// /// The path of the destination container /// protected override void MoveItem(string path, string destination) { // Get type, table name and rowNumber from the path string tableName, destTableName; int rowNumber, destRowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); PathType destType = GetNamesFromPath(destination, out destTableName, out destRowNumber); if (type == PathType.Invalid) { ThrowTerminatingInvalidPathException(path); } if (destType == PathType.Invalid) { ThrowTerminatingInvalidPathException(destination); } if (type == PathType.Table) { ArgumentException e = new ArgumentException("Move not supported for tables"); WriteError(new ErrorRecord(e, "MoveNotSupported", ErrorCategory.InvalidArgument, path)); throw e; } else { OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); OdbcDataAdapter dda = GetAdapterForTable(destTableName); if (dda == null) { return; } DataSet dds = GetDataSetForTable(dda, destTableName); DataTable destTable = GetDataTable(dds, destTableName); DataRow row = table.Rows[rowNumber]; if (destType == PathType.Table) { DataRow destRow = destTable.NewRow(); destRow.ItemArray = row.ItemArray; } else { DataRow destRow = destTable.Rows[destRowNumber]; destRow.ItemArray = row.ItemArray; } // Update the changes if (ShouldProcess(path, "MoveItem")) { WriteItemObject(row, path, false); dda.Update(dds, destTableName); } } } #endregion Navigation #region Helper Methods /// /// Checks if a given path is actually a drive name. /// /// The path to check. /// /// True if the path given represents a drive, false otherwise. /// private bool PathIsDrive(string path) { // Remove the drive name and first path separator. If the // path is reduced to nothing, it is a drive. Also if its // just a drive then there wont be any path separators if (String.IsNullOrEmpty( path.Replace(this.PSDriveInfo.Root, "")) || String.IsNullOrEmpty( path.Replace(this.PSDriveInfo.Root + pathSeparator, "")) ) { return true; } else { return false; } } // PathIsDrive /// /// Breaks up the path into individual elements. /// /// The path to split. /// An array of path segments. private string[] ChunkPath(string path) { // Normalize the path before splitting string normalPath = NormalizePath(path); // Return the path with the drive name and first path // separator character removed, split by the path separator. string pathNoDrive = normalPath.Replace(this.PSDriveInfo.Root + pathSeparator, ""); return pathNoDrive.Split(pathSeparator.ToCharArray()); } // ChunkPath /// /// Adapts the path, making sure the correct path separator /// character is used. /// /// /// private string NormalizePath(string path) { string result = path; if (!String.IsNullOrEmpty(path)) { result = path.Replace("/", pathSeparator); } return result; } // NormalizePath /// /// Ensures that the drive is removed from the specified path /// /// /// Path from which drive needs to be removed /// Path with drive information removed private string RemoveDriveFromPath(string path) { string result = path; string root; if (this.PSDriveInfo == null) { root = String.Empty; } else { root = this.PSDriveInfo.Root; } if (result == null) { result = String.Empty; } if (result.Contains(root)) { result = result.Substring(result.IndexOf(root, StringComparison.OrdinalIgnoreCase) + root.Length); } return result; } /// /// Chunks the path and returns the table name and the row number /// from the path /// /// Path to chunk and obtain information /// Name of the table as represented in the /// path /// Row number obtained from the path /// what the path represents public PathType GetNamesFromPath(string path, out string tableName, out int rowNumber) { PathType retVal = PathType.Invalid; rowNumber = -1; tableName = null; // Check if the path specified is a drive if (PathIsDrive(path)) { return PathType.Database; } // chunk the path into parts string[] pathChunks = ChunkPath(path); switch (pathChunks.Length) { case 1: { string name = pathChunks[0]; if (TableNameIsValid(name)) { tableName = name; retVal = PathType.Table; } } break; case 2: { string name = pathChunks[0]; if (TableNameIsValid(name)) { tableName = name; } int number = SafeConvertRowNumber(pathChunks[1]); if (number >= 0) { rowNumber = number; retVal = PathType.Row; } else { WriteError(new ErrorRecord( new ArgumentException("Row number is not valid"), "RowNumberNotValid", ErrorCategory.InvalidArgument, path)); } } break; default: { WriteError(new ErrorRecord( new ArgumentException("The path supplied has too many segments"), "PathNotValid", ErrorCategory.InvalidArgument, path)); } break; } // switch(pathChunks... return retVal; } // GetNamesFromPath /// /// Throws an argument exception stating that the specified path does /// not represent either a table or a row /// /// path which is invalid private void ThrowTerminatingInvalidPathException(string path) { StringBuilder message = new StringBuilder("Path must represent either a table or a row :"); message.Append(path); throw new ArgumentException(message.ToString()); } /// /// Retrieve the list of tables from the database. /// /// /// Collection of DatabaseTableInfo objects, each object representing /// information about one database table /// internal Collection GetTables() { Collection results = new Collection(); // using ODBC connection to the database and get the schema of tables AccessDBPSDriveInfo di = this.PSDriveInfo as AccessDBPSDriveInfo; if (di == null) { return null; } OdbcConnection connection = di.Connection; DataTable dt = connection.GetSchema("Tables"); int count; // iterate through all rows in the schema and create DatabaseTableInfo // objects which represents a table foreach (DataRow dr in dt.Rows) { String tableName = dr["TABLE_NAME"] as String; DataColumnCollection columns = null; // find the number of rows in the table try { String cmd = "Select count(*) from \"" + tableName + "\""; OdbcCommand command = new OdbcCommand(cmd, connection); count = (Int32)command.ExecuteScalar(); } catch { count = 0; } // create DatabaseTableInfo object representing the table DatabaseTableInfo table = new DatabaseTableInfo(dr, tableName, count, columns); results.Add(table); } // foreach (DataRow... return results; } // GetTables /// /// Return row information from a specified table. /// /// The name of the database table from /// which to retrieve rows. /// Collection of row information objects. public Collection GetRows(string tableName) { Collection results = new Collection(); // Obtain rows in the table and add it to the collection try { OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return null; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); int i = 0; foreach (DataRow row in table.Rows) { results.Add(new DatabaseRowInfo(row, i.ToString(CultureInfo.CurrentCulture))); i++; } // foreach (DataRow... } catch (Exception e) { WriteError(new ErrorRecord(e, "CannotAccessSpecifiedRows", ErrorCategory.InvalidOperation, tableName)); } return results; } // GetRows /// /// Retrieve information about a single table. /// /// The table for which to retrieve /// data. /// Table information. private DatabaseTableInfo GetTable(string tableName) { foreach (DatabaseTableInfo table in GetTables()) { if (String.Equals(tableName, table.Name, StringComparison.OrdinalIgnoreCase)) { return table; } } return null; } // GetTable /// /// Removes the specified table from the database /// /// Name of the table to remove private void RemoveTable(string tableName) { // validate if tablename is valid and if table is present if (String.IsNullOrEmpty(tableName) || !TableNameIsValid(tableName) || !TableIsPresent(tableName)) { return; } // Execute command using ODBC connection to remove a table try { // delete the table using an sql statement string sql = "drop table " + tableName; AccessDBPSDriveInfo di = this.PSDriveInfo as AccessDBPSDriveInfo; if (di == null) { return; } OdbcConnection connection = di.Connection; OdbcCommand cmd = new OdbcCommand(sql, connection); cmd.ExecuteScalar(); } catch (Exception ex) { WriteError(new ErrorRecord(ex, "CannotRemoveSpecifiedTable", ErrorCategory.InvalidOperation, null) ); } } // RemoveTable /// /// Obtain a data adapter for the specified Table /// /// Name of the table to obtain the /// adapter for /// Adapter object for the specified table /// An adapter serves as a bridge between a DataSet (in memory /// representation of table) and the data source internal OdbcDataAdapter GetAdapterForTable(string tableName) { OdbcDataAdapter da = null; AccessDBPSDriveInfo di = this.PSDriveInfo as AccessDBPSDriveInfo; if (di == null || !TableNameIsValid(tableName) || !TableIsPresent(tableName)) { return null; } OdbcConnection connection = di.Connection; try { // Create a odbc data adpater. This can be sued to update the // data source with the records that will be created here // using data sets string sql = "Select * from " + tableName; da = new OdbcDataAdapter(new OdbcCommand(sql, connection)); // Create a odbc command builder object. This will create sql // commands automatically for a single table, thus // eliminating the need to create new sql statements for // every operation to be done. OdbcCommandBuilder cmd = new OdbcCommandBuilder(da); // Set the delete cmd for the table here sql = "Delete from " + tableName + " where ID = ?"; da.DeleteCommand = new OdbcCommand(sql, connection); // Specify a DeleteCommand parameter based on the "ID" // column da.DeleteCommand.Parameters.Add(new OdbcParameter()); da.DeleteCommand.Parameters[0].SourceColumn = "ID"; // Create an InsertCommand based on the sql string // Insert into "tablename" values (?,?,?)" where // ? represents a column in the table. Note that // the number of ? will be equal to the number of // columnds DataSet ds = new DataSet(); ds.Locale = CultureInfo.InvariantCulture; da.FillSchema(ds, SchemaType.Source); sql = "Insert into " + tableName + " values ( "; for (int i = 0; i < ds.Tables["Table"].Columns.Count; i++) { sql += "?, "; } sql = sql.Substring(0, sql.Length - 2); sql += ")"; da.InsertCommand = new OdbcCommand(sql, connection); // Create parameters for the InsertCommand based on the // captions of each column for (int i = 0; i < ds.Tables["Table"].Columns.Count; i++) { da.InsertCommand.Parameters.Add(new OdbcParameter()); da.InsertCommand.Parameters[i].SourceColumn = ds.Tables["Table"].Columns[i].Caption; } // Open the connection if its not already open if (connection.State != ConnectionState.Open) { connection.Open(); } } catch (Exception e) { WriteError(new ErrorRecord(e, "CannotAccessSpecifiedTable", ErrorCategory.InvalidOperation, tableName)); } return da; } // GetAdapterForTable /// /// Gets the DataSet (in memory representation) for the table /// for the specified adapter /// /// Adapter to be used for obtaining /// the table /// Name of the table for which a /// DataSet is required /// The DataSet with the filled in schema internal DataSet GetDataSetForTable(OdbcDataAdapter adapter, string tableName) { Debug.Assert(adapter != null); // Create a dataset object which will provide an in-memory // representation of the data being worked upon in the // data source. DataSet ds = new DataSet(); // Create a table named "Table" which will contain the same // schema as in the data source. //adapter.FillSchema(ds, SchemaType.Source); adapter.Fill(ds, tableName); ds.Locale = CultureInfo.InvariantCulture; return ds; } //GetDataSetForTable /// /// Get the DataTable object which can be used to operate on /// for the specified table in the data source /// /// DataSet object which contains the tables /// schema /// Name of the table /// Corresponding DataTable object representing /// the table /// internal DataTable GetDataTable(DataSet ds, string tableName) { Debug.Assert(ds != null); Debug.Assert(tableName != null); DataTable table = ds.Tables[tableName]; table.Locale = CultureInfo.InvariantCulture; return table; } // GetDataTable /// /// Retrieves a single row from the named table. /// /// The table that contains the /// numbered row. /// The index of the row to return. /// The specified table row. private DatabaseRowInfo GetRow(string tableName, int row) { Collection di = GetRows(tableName); // if the row is invalid write an appropriate error else return the // corresponding row information if (row < di.Count && row >= 0) { return di[row]; } else { WriteError(new ErrorRecord( new ItemNotFoundException(), "RowNotFound", ErrorCategory.ObjectNotFound, row.ToString(CultureInfo.CurrentCulture)) ); } return null; } // GetRow /// /// Method to safely convert a string representation of a row number /// into its Int32 equivalent /// /// String representation of the row /// number /// If there is an exception, -1 is returned private int SafeConvertRowNumber(string rowNumberAsStr) { int rowNumber = -1; try { rowNumber = Convert.ToInt32(rowNumberAsStr, CultureInfo.CurrentCulture); } catch (FormatException fe) { WriteError(new ErrorRecord(fe, "RowStringFormatNotValid", ErrorCategory.InvalidData, rowNumberAsStr)); } catch (OverflowException oe) { WriteError(new ErrorRecord(oe, "RowStringConversionToNumberFailed", ErrorCategory.InvalidData, rowNumberAsStr)); } return rowNumber; } // 1 /// /// Check if a table name is valid /// /// Table name to validate /// Helps to check for SQL injection attacks private bool TableNameIsValid(string tableName) { Regex exp = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); if (exp.IsMatch(tableName)) { return true; } WriteError(new ErrorRecord( new ArgumentException("Table name not valid"), "TableNameNotValid", ErrorCategory.InvalidArgument, tableName)); return false; } // TableNameIsValid /// /// Checks to see if the specified table is present in the /// database /// /// Name of the table to check /// true, if table is present, false otherwise private bool TableIsPresent(string tableName) { // using ODBC connection to the database and get the schema of tables AccessDBPSDriveInfo di = this.PSDriveInfo as AccessDBPSDriveInfo; if (di == null) { return false; } OdbcConnection connection = di.Connection; DataTable dt = connection.GetSchema("Tables"); // check if the specified tableName is available // in the list of tables present in the database foreach (DataRow dr in dt.Rows) { string name = dr["TABLE_NAME"] as string; if (name.Equals(tableName, StringComparison.OrdinalIgnoreCase)) { return true; } } WriteError(new ErrorRecord( new ArgumentException("Specified Table is not present in database"), "TableNotAvailable", ErrorCategory.InvalidArgument, tableName)); return false; }// TableIsPresent /// /// Gets the next available ID in the table /// /// DataTable object representing the table to /// search for ID /// next available id private int GetNextID(DataTable table) { int big = 0; for (int i = 0; i < table.Rows.Count; i++) { DataRow row = table.Rows[i]; int id = (int)row["ID"]; if (big < id) { big = id; } } big++; return big; } #endregion Helper Methods #region Content Methods /// /// Clear the contents at the specified location. In this case, clearing /// the item amounts to clearing a row /// /// The path to the content to clear. public void ClearContent(string path) { string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type != PathType.Table) { WriteError(new ErrorRecord( new InvalidOperationException("Operation not supported. Content can be cleared only for table"), "NotValidRow", ErrorCategory.InvalidArgument, path)); return; } OdbcDataAdapter da = GetAdapterForTable(tableName); if (da == null) { return; } DataSet ds = GetDataSetForTable(da, tableName); DataTable table = GetDataTable(ds, tableName); // Clear contents at the specified location for (int i = 0; i < table.Rows.Count; i++) { table.Rows[i].Delete(); } if (ShouldProcess(path, "ClearContent")) { da.Update(ds, tableName); } } // ClearContent /// /// Not implemented. /// /// /// public object ClearContentDynamicParameters(string path) { return null; } /// /// Get a reader at the path specified. /// /// The path from which to read. /// A content reader used to read the data. public IContentReader GetContentReader(string path) { string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Invalid) { ThrowTerminatingInvalidPathException(path); } else if (type == PathType.Row) { throw new InvalidOperationException("contents can be obtained only for tables"); } return new AccessDBContentReader(path, this); } // GetContentReader /// /// Not implemented. /// /// /// public object GetContentReaderDynamicParameters(string path) { return null; } /// /// Get an object used to write content. /// /// The root path at which to write. /// A content writer for writing. public IContentWriter GetContentWriter(string path) { string tableName; int rowNumber; PathType type = GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Invalid) { ThrowTerminatingInvalidPathException(path); } else if (type == PathType.Row) { throw new InvalidOperationException("contents can be added only to tables"); } return new AccessDBContentWriter(path, this); } /// /// Not implemented. /// /// /// public object GetContentWriterDynamicParameters(string path) { return null; } #endregion Content Methods #region Private Properties private string pathSeparator = "\\"; private static string pattern = @"^[a-z]+[0-9]*_*$"; #endregion Private Properties } // AccessDBProvider #endregion AccessDBProvider #region Helper Classes #region Public Enumerations /// /// Type of item represented by the path /// public enum PathType { /// /// Represents a database /// Database, /// /// Represents a table /// Table, /// /// Represents a row /// Row, /// /// Represents an invalid path /// Invalid }; #endregion Public Enumerations #region AccessDBPSDriveInfo /// /// Any state associated with the drive should be held here. /// In this case, it's the connection to the database. /// internal class AccessDBPSDriveInfo : PSDriveInfo { private OdbcConnection connection; /// /// ODBC connection information. /// public OdbcConnection Connection { get { return connection; } set { connection = value; } } /// /// Constructor that takes one argument /// /// Drive provided by this provider public AccessDBPSDriveInfo(PSDriveInfo driveInfo) : base(driveInfo) { } } // class AccessDBPSDriveInfo #endregion AccessDBPSDriveInfo #region DatabaseTableInfo /// /// Contains information specific to the database table. /// Similar to the DirectoryInfo class. /// public class DatabaseTableInfo { /// /// Row from the "tables" schema /// public DataRow Data { get { return data; } set { data = value; } } private DataRow data; /// /// The table name. /// public string Name { get { return name; } set { name = value; } } private String name; /// /// The number of rows in the table. /// public int RowCount { get { return rowCount; } set { rowCount = value; } } private int rowCount; /// /// The column definitions for the table. /// public DataColumnCollection Columns { get { return columns; } set { columns = value; } } private DataColumnCollection columns; /// /// Constructor. /// /// The row definition. /// The table name. /// The number of rows in the table. /// Information on the column tables. public DatabaseTableInfo(DataRow row, string name, int rowCount, DataColumnCollection columns) { Name = name; Data = row; RowCount = rowCount; Columns = columns; } // DatabaseTableInfo } // class DatabaseTableInfo #endregion DatabaseTableInfo #region DatabaseRowInfo /// /// Contains information specific to an individual table row. /// Analogous to the FileInfo class. /// public class DatabaseRowInfo { /// /// Row data information. /// public DataRow Data { get { return data; } set { data = value; } } private DataRow data; /// /// The row index. /// public string RowNumber { get { return rowNumber; } set { rowNumber = value; } } private string rowNumber; /// /// Constructor. /// /// The row information. /// The row index. public DatabaseRowInfo(DataRow row, string name) { RowNumber = name; Data = row; } // DatabaseRowInfo } // class DatabaseRowInfo #endregion DatabaseRowInfo #region AccessDBContentReader /// /// Content reader used to retrieve data from this provider. /// public class AccessDBContentReader : IContentReader { // A provider instance is required so as to get "content" private AccessDBProvider provider; private string path; private long currentOffset; internal AccessDBContentReader(string path, AccessDBProvider provider) { this.path = path; this.provider = provider; } /// /// Read the specified number of rows from the source. /// /// The number of items to /// return. /// An array of elements read. public IList Read(long readCount) { // Read the number of rows specified by readCount and increment // offset string tableName; int rowNumber; PathType type = provider.GetNamesFromPath(path, out tableName, out rowNumber); Collection rows = provider.GetRows(tableName); Collection results = new Collection(); if (currentOffset < 0 || currentOffset >= rows.Count) { return null; } int rowsRead = 0; while (rowsRead < readCount && currentOffset < rows.Count) { results.Add(rows[(int)currentOffset].Data); rowsRead++; currentOffset++; } return results; } // Read /// /// Moves the content reader specified number of rows from the /// origin /// /// Number of rows to offset /// Starting row from which to offset public void Seek(long offset, System.IO.SeekOrigin origin) { // get the number of rows in the table which will help in // calculating current position string tableName; int rowNumber; PathType type = provider.GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Invalid) { throw new ArgumentException("Path specified must represent a table or a row :" + path); } if (type == PathType.Table) { Collection rows = provider.GetRows(tableName); int numRows = rows.Count; if (offset > rows.Count) { throw new ArgumentException( "Offset cannot be greater than the number of rows available" ); } if (origin == System.IO.SeekOrigin.Begin) { // starting from Beginning with an index 0, the current offset // has to be advanced to offset - 1 currentOffset = offset - 1; } else if (origin == System.IO.SeekOrigin.End) { // starting from the end which is numRows - 1, the current // offset is so much less than numRows - 1 currentOffset = numRows - 1 - offset; } else { // calculate from the previous value of current offset // advancing forward always currentOffset += offset; } } // if (type... else { // for row, the offset will always be set to 0 currentOffset = 0; } } // Seek /// /// Closes the content reader, so all members are reset /// public void Close() { Dispose(); } // Close /// /// Dispose any resources being used /// public void Dispose() { Seek(0, System.IO.SeekOrigin.Begin); GC.SuppressFinalize(this); } // Dispose } // AccessDBContentReader #endregion AccessDBContentReader #region AccessDBContentWriter /// /// Content writer used to write data in this provider. /// public class AccessDBContentWriter : IContentWriter { // A provider instance is required so as to get "content" private AccessDBProvider provider; private string path; private long currentOffset; internal AccessDBContentWriter(string path, AccessDBProvider provider) { this.path = path; this.provider = provider; } /// /// Write the specified row contents in the source /// /// The contents to be written to the source. /// /// An array of elements which were successfully written to /// the source /// public IList Write(IList content) { if (content == null) { return null; } // Get the total number of rows currently available it will // determine how much to overwrite and how much to append at // the end string tableName; int rowNumber; PathType type = provider.GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Table) { OdbcDataAdapter da = provider.GetAdapterForTable(tableName); if (da == null) { return null; } DataSet ds = provider.GetDataSetForTable(da, tableName); DataTable table = provider.GetDataTable(ds, tableName); string[] colValues = (content[0] as string).Split(','); // set the specified row DataRow row = table.NewRow(); for (int i = 0; i < colValues.Length; i++) { if (!String.IsNullOrEmpty(colValues[i])) { row[i] = colValues[i]; } } //table.Rows.InsertAt(row, rowNumber); // Update the table table.Rows.Add(row); da.Update(ds, tableName); } else { throw new InvalidOperationException("Operation not supported. Content can be added only for tables"); } return null; } // Write /// /// Moves the content reader specified number of rows from the /// origin /// /// Number of rows to offset /// Starting row from which to offset public void Seek(long offset, System.IO.SeekOrigin origin) { // get the number of rows in the table which will help in // calculating current position string tableName; int rowNumber; PathType type = provider.GetNamesFromPath(path, out tableName, out rowNumber); if (type == PathType.Invalid) { throw new ArgumentException("Path specified should represent either a table or a row : " + path); } Collection rows = provider.GetRows(tableName); int numRows = rows.Count; if (offset > rows.Count) { throw new ArgumentException( "Offset cannot be greater than the number of rows available" ); } if (origin == System.IO.SeekOrigin.Begin) { // starting from Beginning with an index 0, the current offset // has to be advanced to offset - 1 currentOffset = offset - 1; } else if (origin == System.IO.SeekOrigin.End) { // starting from the end which is numRows - 1, the current // offset is so much less than numRows - 1 currentOffset = numRows - 1 - offset; } else { // calculate from the previous value of current offset // advancing forward always currentOffset += offset; } } // Seek /// /// Closes the content reader, so all members are reset /// public void Close() { Dispose(); } // Close /// /// Dispose any resources being used /// public void Dispose() { Seek(0, System.IO.SeekOrigin.Begin); GC.SuppressFinalize(this); } // Dispose } // AccessDBContentWriter #endregion AccessDBContentWriter #endregion Helper Classes } // namespace Microsoft.Samples.PowerShell.Providers