Jo Shields a575963da9 Imported Upstream version 3.6.0
Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
2014-08-13 10:39:27 +01:00

597 lines
17 KiB
C#

//
// MetaTable.cs
//
// Author:
// Atsushi Enomoto <atsushi@ximian.com>
// Marek Habersack <mhabersack@novell.com>
//
// Copyright (C) 2008-2009 Novell Inc. http://novell.com
//
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Security.Permissions;
using System.Security.Principal;
using System.Text;
using System.Web.Caching;
using System.Web.UI;
using System.Web.Routing;
using System.Web.DynamicData.ModelProviders;
namespace System.Web.DynamicData
{
[AspNetHostingPermission (SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[AspNetHostingPermission (SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
public class MetaTable
{
RouteCollection routes;
MetaColumn displayColumn;
MetaColumn sortColumn;
bool? entityHasToString;
bool? sortDescending;
global::System.ComponentModel.AttributeCollection attributes;
string displayName;
bool displayColumnChecked;
bool sortColumnChecked;
internal MetaTable (MetaModel model, TableProvider provider, ContextConfiguration configuration)
{
bool scaffoldAllTables;
this.model = model;
Provider = provider;
if (configuration != null) {
ScaffoldAllTables = scaffoldAllTables = configuration.ScaffoldAllTables;
Func <Type, TypeDescriptionProvider> factory = configuration.MetadataProviderFactory;
if (factory != null) {
Type t = EntityType;
TypeDescriptionProvider p = factory (t);
if (p != null)
TypeDescriptor.AddProvider (p, t);
}
} else
scaffoldAllTables = false;
ScaffoldTableAttribute attr = null;
MetaModel.GetDataFieldAttribute <ScaffoldTableAttribute> (Attributes, ref attr);
Scaffold = attr != null ? attr.Scaffold : scaffoldAllTables;
DataContextType = provider.DataModel.ContextType;
var columns = new List <MetaColumn> ();
var primaryKeyColumns = new List <MetaColumn> ();
var foreignKeyColumnNames = new List <string> ();
MetaColumn mc;
foreach (var c in provider.Columns) {
// this seems to be the determining factor on whether we create
// MetaColumn or MetaForeignKeyColumn/MetaChildrenColumn. As the
// determination depends upon the relationship direction, we must
// check that using the ColumnProvider's association, if any.
//
// http://msdn.microsoft.com/en-us/library/system.web.dynamicdata.metaforeignkeycolumn.aspx
// http://msdn.microsoft.com/en-us/library/system.web.dynamicdata.metachildrencolumn.aspx
// http://forums.asp.net/t/1426992.aspx
var association = c.Association;
if (association == null)
mc = new MetaColumn (this, c);
else {
var dir = association.Direction;
if (dir == AssociationDirection.OneToOne || dir == AssociationDirection.ManyToOne)
mc = new MetaForeignKeyColumn (this, c);
else
mc = new MetaChildrenColumn (this, c);
}
columns.Add (mc);
if (c.IsPrimaryKey)
primaryKeyColumns.Add (mc);
if (mc is MetaForeignKeyColumn)
foreignKeyColumnNames.Add (c.Name);
}
Columns = new ReadOnlyCollection <MetaColumn> (columns);
PrimaryKeyColumns = new ReadOnlyCollection <MetaColumn> (primaryKeyColumns);
if (foreignKeyColumnNames.Count == 0)
ForeignKeyColumnsNames = String.Empty;
else
ForeignKeyColumnsNames = String.Join (",", foreignKeyColumnNames.ToArray ());
HasPrimaryKey = primaryKeyColumns.Count > 0;
// See http://forums.asp.net/t/1388561.aspx
//
// Also, http://forums.asp.net/t/1307243.aspx - that seems to be out of
// scope for us, though (at least for now)
IsReadOnly = primaryKeyColumns.Count == 0;
// FIXME: fill more properties.
}
MetaModel model;
public global::System.ComponentModel.AttributeCollection Attributes {
get {
if (attributes == null) {
ICustomTypeDescriptor descriptor = MetaModel.GetTypeDescriptor (EntityType);
if (descriptor != null)
attributes = descriptor.GetAttributes ();
}
return attributes;
}
}
public ReadOnlyCollection<MetaColumn> Columns { get; private set; }
public string DataContextPropertyName {
get { return Provider.Name; }
}
public Type DataContextType { get; private set; }
public MetaColumn DisplayColumn {
get {
if (displayColumn == null)
displayColumn = FindDisplayColumn ();
return displayColumn;
}
}
public string DisplayName {
get {
if (displayName == null)
displayName = DetermineDisplayName ();
return displayName;
}
}
public Type EntityType {
get { return Provider.EntityType; }
}
public string ForeignKeyColumnsNames { get; private set; }
public bool HasPrimaryKey { get; private set; }
public bool IsReadOnly { get; private set; }
public string ListActionPath {
get { return GetActionPath (PageAction.List); }
}
public MetaModel Model {
get { return model; }
}
public string Name {
get { return Provider.Name; }
}
public ReadOnlyCollection<MetaColumn> PrimaryKeyColumns { get; private set; }
public TableProvider Provider { get; private set; }
public bool Scaffold { get; private set; }
internal bool ScaffoldAllTables { get; private set; }
public MetaColumn SortColumn {
get {
if (sortColumn == null)
sortColumn = FindSortColumn ();
return sortColumn;
}
}
public bool SortDescending {
get {
if (sortDescending == null)
sortDescending = DetermineSortDescending ();
return (bool)sortDescending;
}
}
string BuildActionPath (string path, RouteValueDictionary values)
{
var sb = new StringBuilder ();
sb.Append (path);
if (values != null && values.Count > 0) {
sb.Append ('?');
bool first = true;
foreach (var de in values) {
if (first)
first = false;
else
sb.Append ('&');
sb.Append (Uri.EscapeDataString (de.Key));
sb.Append ('=');
object parameterValue = de.Value;
if (parameterValue != null)
sb.Append (Uri.EscapeDataString (parameterValue.ToString ()));
}
}
return sb.ToString ();
}
public object CreateContext ()
{
return Activator.CreateInstance (DataContextType);
}
string DetermineDisplayName ()
{
DisplayNameAttribute attr = null;
MetaModel.GetDataFieldAttribute <DisplayNameAttribute> (Attributes, ref attr);
if (attr == null)
return Name;
return attr.DisplayName;
}
bool DetermineSortDescending ()
{
DisplayColumnAttribute attr = null;
MetaModel.GetDataFieldAttribute <DisplayColumnAttribute> (Attributes, ref attr);
if (attr == null)
return false;
return attr.SortDescending;
}
void FillWithPrimaryKeys (RouteValueDictionary values, IList<object> primaryKeyValues)
{
if (primaryKeyValues == null)
return;
ReadOnlyCollection <MetaColumn> pkc = PrimaryKeyColumns;
int pkcCount = pkc.Count;
// Fill the above with primary keys using primaryKeyValues - .NET does not
// check (again) whether there are enough elements in primaryKeyValues, it
// just assumes there are at least as many of them as in the
// PrimaryKeyColumns collection, so we'll emulate this bug here.
if (primaryKeyValues.Count < pkc.Count)
throw new ArgumentOutOfRangeException ("index");
// This is so wrong (the generated URL might contain values assigned to
// primary column keys which do not correspond with the column's type) but
// this is how the upstream behaves, unfortunately.
for (int i = 0; i < pkcCount; i++)
values.Add (pkc [i].Name, primaryKeyValues [i]);
}
MetaColumn FindDisplayColumn ()
{
if (displayColumnChecked)
return displayColumn;
displayColumnChecked = true;
ReadOnlyCollection<MetaColumn> columns = Columns;
// 1. The column that is specified by using the DisplayColumnAttribute attribute.
DisplayColumnAttribute attr = Attributes [typeof (DisplayColumnAttribute)] as DisplayColumnAttribute;
if (attr != null) {
string name = attr.DisplayColumn;
foreach (MetaColumn mc in columns)
if (String.Compare (name, mc.Name, StringComparison.Ordinal) == 0)
return mc;
throw new InvalidOperationException ("The display column '" + name + "' specified for the table '" + EntityType.Name + "' does not exist.");
}
// 2. The first string column that is not in the primary key.
// LAMESPEC: also a column which is not a custom one
ReadOnlyCollection <MetaColumn> pkc = PrimaryKeyColumns;
bool havePkc = pkc.Count > 0;
foreach (MetaColumn mc in columns) {
if (mc.IsCustomProperty || (havePkc && pkc.Contains (mc)))
continue;
if (mc.ColumnType == typeof (string))
return mc;
}
// 3. The first string column that is in the primary key.
if (havePkc) {
foreach (MetaColumn mc in pkc) {
if (mc.ColumnType == typeof (string))
return mc;
}
// 4. The first non-string column that is in the primary key.
return pkc [0];
}
// No check, again, is made whether the columns collection contains enough
// columns to perform successful lookup, we're emulating that here.
if (columns.Count == 0)
throw new ArgumentOutOfRangeException ("index");
// Fallback - return the first column
return columns [0];
}
MetaColumn FindSortColumn ()
{
if (sortColumnChecked)
return sortColumn;
sortColumnChecked = true;
DisplayColumnAttribute attr = Attributes [typeof (DisplayColumnAttribute)] as DisplayColumnAttribute;
if (attr == null)
return null;
string name = attr.SortColumn;
if (String.IsNullOrEmpty (name))
return null;
MetaColumn ret = null;
Exception exception = null;
try {
ret = Columns.First <MetaColumn> ((MetaColumn mc) => {
if (String.Compare (mc.Name, name, StringComparison.Ordinal) == 0)
return true;
return false;
});
} catch (Exception ex) {
exception = ex;
}
if (ret == null)
throw new InvalidOperationException ("The sort column '" + name + "' specified for table '" + Name + "' does not exist.", exception);
return ret;
}
public string GetActionPath (string action)
{
// You can see this is the call we should make by modifying one of the unit
// tests (e.g. MetaTableTest.GetActinPath) and commenting out the line which
// assigns a context to HttpContext.Current and looking at the exception
// stack trace.
return GetActionPath (action, (IList <object>)null);
}
public string GetActionPath (string action, IList<object> primaryKeyValues)
{
if (String.IsNullOrEmpty (action))
return String.Empty;
var values = new RouteValueDictionary ();
values.Add ("Action", action);
values.Add ("Table", Name);
FillWithPrimaryKeys (values, primaryKeyValues);
// To see that this internal method is called, comment out setting of
// HttpContext in the GetActionPath_Action_PrimaryKeyValues test and look at
// the stack trace
return GetActionPathFromRoutes (values);
}
public string GetActionPath (string action, object row)
{
// To see that this method is called, comment out setting of
// HttpContext in the GetActionPath_Action_Row test and look at
// the stack trace
return GetActionPath (action, GetPrimaryKeyValues (row));
}
public string GetActionPath (string action, RouteValueDictionary routeValues)
{
if (String.IsNullOrEmpty (action))
return String.Empty;
// .NET doesn't check whether routeValues is null, we'll just "implement"
// the behavior here...
if (routeValues == null)
throw new NullReferenceException ();
// NO check is made whether those two are already in the dictionary...
routeValues.Add ("Action", action);
routeValues.Add ("Table", Name);
// To see that this internal method is called, comment out setting of
// HttpContext in the GetActionPath_Action_RouteValues test and look at
// the stack trace
return GetActionPathFromRoutes (routeValues);
}
public string GetActionPath (string action, IList<object> primaryKeyValues, string path)
{
if (String.IsNullOrEmpty (path))
return GetActionPath (action, primaryKeyValues);
var values = new RouteValueDictionary ();
FillWithPrimaryKeys (values, primaryKeyValues);
return BuildActionPath (path, values);
}
public string GetActionPath (string action, object row, string path)
{
return GetActionPath (action, GetPrimaryKeyValues (row), path);
}
string GetActionPathFromRoutes (RouteValueDictionary values)
{
if (routes == null)
routes = RouteTable.Routes;
VirtualPathData vpd = routes.GetVirtualPath (DynamicDataRouteHandler.GetRequestContext (HttpContext.Current), values);
return vpd == null ? String.Empty : vpd.VirtualPath;
}
public MetaColumn GetColumn (string columnName)
{
MetaColumn mc;
if (TryGetColumn (columnName, out mc))
return mc;
throw new InvalidOperationException (String.Format ("Column '{0}' does not exist in the meta table '{1}'", columnName, Name));
}
public string GetDisplayString (object row)
{
if (row == null)
return String.Empty;
if (entityHasToString == null) {
Type type = EntityType;
MethodInfo pi = type == null ? null : type.GetMethod ("ToString", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
entityHasToString = pi != null;
}
if ((bool)entityHasToString)
return row.ToString ();
// Once again no check is made for row's type
MetaColumn mc = DisplayColumn;
object value = DataBinder.GetPropertyValue (row, mc.Name);
if (value == null)
return String.Empty;
return value.ToString ();
}
public string GetPrimaryKeyString (IList<object> primaryKeyValues)
{
if (primaryKeyValues == null || primaryKeyValues.Count == 0)
return String.Empty;
var strings = new List <string> ();
bool allNull = true;
foreach (object o in primaryKeyValues) {
if (o == null) {
strings.Add (null);
continue;
}
var str = o.ToString ();
if (str == null) {
strings.Add (null);
continue;
}
allNull = false;
strings.Add (str);
}
if (allNull) {
strings = null;
return String.Empty;
}
var ret = String.Join (",", strings.ToArray ());
strings = null;
return ret;
}
public string GetPrimaryKeyString (object row)
{
return GetPrimaryKeyString (GetPrimaryKeyValues (row));
}
public IList<object> GetPrimaryKeyValues (object row)
{
if (row == null)
return null;
ReadOnlyCollection <MetaColumn> pkc = PrimaryKeyColumns;
int pkcCount = pkc.Count;
var ret = new List <object> ();
if (pkcCount == 0)
return ret;
// No check is made whether row is of correct type,
// DataBinder.GetPropertyValue is called instead to fetch value of each
// member of the row object corresponding to primary key columns.
for (int i = 0; i < pkcCount; i++)
ret.Add (DataBinder.GetPropertyValue (row, pkc [i].Name));
return ret;
}
public IQueryable GetQuery ()
{
return GetQuery (CreateContext ());
}
public IQueryable GetQuery (object context)
{
return Provider.GetQuery (context == null ? CreateContext () : context);
}
internal void Init ()
{
ReadOnlyCollection <MetaColumn> columns = Columns;
if (columns == null)
return;
foreach (MetaColumn mc in columns)
mc.Init ();
}
public override string ToString ()
{
return Name;
}
public bool TryGetColumn (string columnName, out MetaColumn column)
{
if (columnName == null)
throw new ArgumentNullException ("columnName");
foreach (var m in Columns)
if (m.Name == columnName) {
column = m;
return true;
}
column = null;
return false;
}
}
}