// Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using OpenTap.Addin.Annotation;
using OpenTap.Translation;
namespace OpenTap
{
/// Annotators can be used to annotation objects with data for display.
public interface IAnnotator : ITapPlugin
{
/// The priority of this annotator. Specifies which orders annotations are added.
double Priority { get; }
/// Implements annotation for an object.
/// The current collection of annotations for an object. This method can add to the collection.
void Annotate(AnnotationCollection annotations);
}
///
/// Marker interface to indicate that a type represents annotation data for an object.
///
public interface IAnnotation { }
/// Specifies how a display is implemented and presented to user
public interface IDisplayAnnotation : IAnnotation
{
/// Optional text that provides a description of the item.
string Description { get; }
/// Optional text used to group displayed items.
string[] Group { get; }
/// Name displayed by the UI.
string Name { get; }
/// Optional double that ranks items and groups in ascending order relative to other items/groups.
/// Default is -10000. For a group, the order is the average order of the elements inside the group.
/// Any double value is allowed. Items with same order are ranked alphabetically.
///
double Order { get; }
/// Boolean setting that indicates whether a group's default appearance is collapsed.
bool Collapsed { get; }
}
/// Gets or sets the value of a thing.
public interface IObjectValueAnnotation : IAnnotation
{
/// Gets or sets the current value. Note, for the value to be written to the owner object, Annotation.Write has to be called.
object Value { get; set; }
}
///
/// A marker interface for object value annotations that comes from a merged source instead of a single-value source.
///
public interface IMergedValueAnnotation : IObjectValueAnnotation
{
}
/// Specifies how available values proxies are implemented. This class should rarely be implemented. Consider implementing just IAvailableValuesAnnotation instead.
public interface IAvailableValuesAnnotationProxy : IAnnotation
{
/// Annotated available values.
IEnumerable AvailableValues { get; }
/// Annotated selected value. Note this should belong to the set of AvailableValues as well.
AnnotationCollection SelectedValue { get; set; }
}
/// Specifies how suggested value proxies are implemented. This class should rarely be implemented. Consider implementing just ISuggestedValuesAnnotation instead.
public interface ISuggestedValuesAnnotationProxy : IAnnotation
{
///
/// Annotated suggested values.
///
IEnumerable SuggestedValues { get; }
///
/// Annotated selected value.
///
AnnotationCollection SelectedValue { get; set; }
}
/// Specifies how multi selection annotation proxies are implemented. Not this should rarely need to be implemented
public interface IMultiSelectAnnotationProxy : IAnnotation
{
/// The annotated selected values.
IEnumerable SelectedValues { get; set; }
}
///
/// Defines a available values implementation. Implement this to extend the data annotation system with a new available values.
///
public interface IAvailableValuesAnnotation : IAnnotation
{
/// The available values.
IEnumerable AvailableValues { get; }
}
///
/// Enhances the IAvailableValuesAnnotation with a 'SelectedValue'. Having this ensures that objects that has been transformed can get read back in the correct way.
///
interface IAvailableValuesSelectedAnnotation : IAvailableValuesAnnotation
{
/// Gets or sets the selected value.
object SelectedValue { get; set; }
}
/// Defines a suggested values implementation.
public interface ISuggestedValuesAnnotation : IAnnotation
{
/// The currently suggested values
IEnumerable SuggestedValues { get; }
}
///
/// Defines a string value annotation implementation. This can be implemented for any type which can be converted to/from a string value. Note: IStringReadOnlyValueAnnotation can be implemented in the read-only case.
///
public interface IStringValueAnnotation : IStringReadOnlyValueAnnotation
{
/// The string value representation of an object. The setter can throw an exception if the format is not correctly used.
new string Value { get; set; }
}
///
/// If the object value is based on copying values, some performance optimizations can be done, so these string value annotations can be marked with this interface.
///
interface ICopyStringValueAnnotation : IStringValueAnnotation
{
}
/// Defines a read-only string value annotation implementation.
public interface IStringReadOnlyValueAnnotation : IAnnotation
{
/// The string value representation of the object.
string Value { get; }
}
/// Makes it possible to get an example of a value from a property.
public interface IStringExampleValueAnnotation : IAnnotation
{
/// Gets an example of what the current value could be.
string Example { get; }
}
/// Defines how an error annotation works. Note: Multiple of IErrorAnnotation can be used in the same annotation. In this case the errors will be concatenated.
public interface IErrorAnnotation : IAnnotation
{
/// The list of errors for this annotation.
IEnumerable Errors { get; }
}
/// Specifies the access to an annotation.
public interface IAccessAnnotation : IAnnotation
{
/// Gets if the annotation is read-only. This state can be temporary or permanent.
bool IsReadOnly { get; }
/// Gets if the annotation is visible. This state can be temporary or permanent.
bool IsVisible { get; }
}
///
/// Owned annotations interacts directly with the source object. It is updated through the Read operation and changes are written with the Write operation. Specialized knowledge about the object is needed for implementation.
///
public interface IOwnedAnnotation : IAnnotation
{
/// Read changes from the source.
///
void Read(object source);
/// Write changes to the source.
///
void Write(object source);
}
/// Marks that an annotation reflects a member of an object.
public interface IMemberAnnotation : IReflectionAnnotation
{
/// Gets the member.
IMemberData Member { get; }
}
/// Reflects the type of the object value being annotated.
public interface IReflectionAnnotation : IAnnotation
{
/// The reflection info object.
ITypeData ReflectionInfo { get; }
}
/// The object can be used for multi select operations. Example: FlagAttribute enums can be multi-selected.
public interface IMultiSelect : IAnnotation
{
/// The currently selected values.
IEnumerable Selected { get; set; }
}
/// The annotation can be invoked to do some action.
public interface IMethodAnnotation : IAnnotation
{
/// Invokes the action.
void Invoke();
}
///
/// The merged method annotation marks a custom method annotation which overrides
/// the standard behavior in the case where multiple values are merged.
/// This can happen during multi select of a test step with a method.
///
public interface IMergedMethodAnnotation : IMethodAnnotation
{
}
/// Specifies how to implement basic collection annotations.
public interface IBasicCollectionAnnotation : IAnnotation
{
/// he currently selected elements in the list.
IEnumerable Elements { get; set; }
}
/// Used to mark a collection as fixed-size.
public interface IFixedSizeCollectionAnnotation : IAnnotation
{
/// Gets if the collection annotated is fixed size.
bool IsFixedSize { get; }
}
/// Specifies that the annotation reflects some kind of collection.
public interface ICollectionAnnotation : IAnnotation
{
/// The reflected elements.
IEnumerable AnnotatedElements { get; set; }
/// Creates a new element that can be put into the collection. Note that initially the element should not be added to the collection. This task is for the user.
/// The new element.
AnnotationCollection NewElement();
}
/// The annotation reflects multiple members on an object.
public interface IMembersAnnotation : IAnnotation
{
/// The reflected members.
IEnumerable Members { get; }
}
/// Like IMembersAnnotation, but a specific member can be fetched.
public interface INamedMembersAnnotation : IAnnotation
{
/// Returns the annotated member.
///
///
AnnotationCollection GetMember(IMemberData name);
}
/// Marks that a property should be ignored when annotating members.
/// This can be applied as an optimization to properties in order to improve annotation performance.
[AttributeUsage(AttributeTargets.Property )]
public class AnnotationIgnoreAttribute : Attribute
{
}
/// Can be used to forward a set of members from one annotation to another.
public interface IForwardedAnnotations : IAnnotation
{
/// The forwarded annotations.
IEnumerable Forwarded { get; }
}
/// Interface for providing annotations with a way of explaining the value.
public interface IValueDescriptionAnnotation : IAnnotation
{
/// Description of a value.
/// A string describing the current value.
string Describe();
}
/// Annotation for marking something as enabled or disabled.
public interface IEnabledAnnotation : IAnnotation
{
/// Gets if an annotation is enabled.
bool IsEnabled { get; }
}
///
/// Annotates that a member is read only.
///
public class ReadOnlyMemberAnnotation : IAccessAnnotation
{
/// Always returns true.
public bool IsReadOnly => true;
/// Always returns true.
public bool IsVisible => true;
}
class MembersAnnotation : INamedMembersAnnotation, IMembersAnnotation, IOwnedAnnotation
{
Dictionary members = new Dictionary();
IEnumerable getMembers()
{
var val2 = fac.Get().Value;
var val = fac.Get();
IEnumerable _members;
if (val2 != null)
_members = TypeData.GetTypeData(val2).GetMembers();
else _members = val.ReflectionInfo.GetMembers();
var cnt = _members.Count();
if (members.Count == cnt) return members.Values;
if (members.Count == 0)
members = new Dictionary(cnt);
var members2 = val.ReflectionInfo.GetMembers();
foreach (var item in members2)
{
if (item.HasAttribute()) continue;
if (item.Readable == false) continue;
GetMember(item);
}
return members.Values;
}
public IEnumerable Members => getMembers();
readonly AnnotationCollection fac;
public MembersAnnotation(AnnotationCollection fac)
{
this.fac = fac;
}
public void Read(object source)
{
var val = fac.Get()?.Value;
if (val == null) return;
foreach (var mem in members)
{
mem.Value.Read(val);
}
}
public void Write(object source)
{
var val = fac.Get()?.Value;
if (val == null) return;
foreach (var mem in members)
{
mem.Value.Write(val);
}
}
public AnnotationCollection GetMember(IMemberData member)
{
if (members.TryGetValue(member, out AnnotationCollection value)) return value;
var objectValue = fac.Get().Value;
var annotation = fac.AnnotateMember(member, objectValue);
members[member] = annotation;
if (objectValue != null)
{
annotation.Read(objectValue);
}
return annotation;
}
}
class EnabledIfAnnotation : IAccessAnnotation, IOwnedAnnotation, IEnabledAnnotation
{
public bool IsReadOnly
{
get
{
doRead();
return isReadOnly;
}
}
public bool IsVisible
{
get
{
doRead();
return isVisible;
}
}
bool isReadOnly;
bool isVisible;
object source;
void doRead()
{
if (source != null)
{
isReadOnly = !EnabledIfAttribute
.IsEnabled(mem.Member, source, out IMemberData _, out IComparable __, out bool hidden);
isVisible = !hidden;
source = null;
}
}
public void Read(object source)
{
this.source = source;
}
public void Write(object source)
{
}
IMemberAnnotation mem;
public EnabledIfAnnotation(IMemberAnnotation mem)
{
this.mem = mem;
}
public bool IsEnabled => IsReadOnly == false;
}
class ValidationErrorAnnotation : IErrorAnnotation, IOwnedAnnotation
{
IMemberAnnotation mem;
string error;
public ValidationErrorAnnotation(IMemberAnnotation mem)
{
this.mem = mem;
}
public IEnumerable Errors
{
get
{
DoRead();
if (string.IsNullOrWhiteSpace(error) == false)
return new[] { error };
return Array.Empty();
}
}
object source;
void DoRead()
{
var source = this.source;
var mem = this.mem.Member;
// Special case to add support for EmbeddedMemberData.
// The embedded member may be nested in multiple layers
// of embeddings normally it is just one level though.
// iterate to grab the innermost source and member.
while (mem is EmbeddedMemberData m2)
{
if (source == null) return;
source = m2.OwnerMember.GetValue(source);
mem = m2.InnerMember;
}
if (source is IDataErrorInfo dataErrorInfo)
{
try
{
error = dataErrorInfo[mem.Name];
}
catch (Exception e)
{
error = e.Message;
}
// set source to null to signal that errors has been read this time.
}
this.source = null;
}
public void Read(object source) => this.source = source;
public void Write(object source) { }
}
class NumberAnnotation : IStringValueAnnotation, IErrorAnnotation, ICopyStringValueAnnotation
{
public Type NullableType { get; set; }
string currentError;
public string Value
{
get
{
var value = annotation.Get();
if (value != null)
{
var unit = annotation.Get();
var value2 = value.Value;
if (NullableType != null && value2 == null)
return "";
return new NumberFormatter(CultureInfo.CurrentCulture, unit).FormatNumber(value2);
}
return null;
}
set
{
if (NullableType != null && value == "")
{
var val = annotation.Get();
val.Value = null;
return;
}
currentError = null;
var unit = annotation.Get();
if (annotation.Get()?.ReflectionInfo is TypeData cst)
{
object number = null;
try
{
number = new NumberFormatter(CultureInfo.CurrentCulture, unit).ParseNumber(value, NullableType ?? cst.Type);
}
catch (Exception e)
{
currentError = e.Message;
}
if (number != null)
{
var val = annotation.Get();
val.Value = number;
}
}
else
{
throw new InvalidOperationException("Number converter supports only C# types");
}
}
}
AnnotationCollection annotation;
public NumberAnnotation(AnnotationCollection mem)
{
this.annotation = mem;
}
public IEnumerable Errors => currentError == null ? Array.Empty() : new[] { currentError };
}
class TimeSpanAnnotation : IStringValueAnnotation, ICopyStringValueAnnotation, IErrorAnnotation
{
public string Value
{
get
{
if (annotation.Get(from: this).Value is TimeSpan timespan)
return TimeSpanFormatter.Format(timespan, fmt.Verbosity);
return "";
}
set
{
try
{
var timespan = TimeSpanParser.Parse(value);
annotation.Get(from: this).Value = timespan;
Errors = [];
}
catch (Exception ex)
{
Errors = [ex.Message];
}
}
}
AnnotationCollection annotation;
TimeSpanFormatAttribute fmt;
public TimeSpanAnnotation(AnnotationCollection annotation)
{
fmt = annotation?.Get()?.Member.GetAttribute();
if (fmt == null) fmt = new TimeSpanFormatAttribute();
this.annotation = annotation;
}
public IEnumerable Errors { get; private set; }
}
class NumberSequenceAnnotation : IStringValueAnnotation, ICopyStringValueAnnotation, IErrorAnnotation
{
string currentError;
public IEnumerable Errors => currentError == null ? Array.Empty() : new[] { currentError };
public string Value
{
get
{
var member = mem.Get();
var value = (IEnumerable)member.Value;
if (value == null) return "";
var unit = mem.Get();
return new NumberFormatter(System.Globalization.CultureInfo.CurrentCulture, unit).FormatRange(value);
}
set
{
var objVal = mem.Get();
var reflect = mem.Get();
var unit = mem.Get();
if (reflect.ReflectionInfo is TypeData cst)
{
currentError = null;
try
{
var numbers = DoConvertBack(value, cst.Type, unit, CultureInfo.CurrentCulture);
objVal.Value = numbers;
}
catch (Exception e)
{
currentError = e.Message;
}
}
else
{
throw new InvalidOperationException("Number converter supports only C# types");
}
}
}
public object DoConvertBack(object _value, Type targetType, UnitAttribute unit, CultureInfo culture)
{
string value = _value as string;
if (value == null)
return null;
Type elementType = targetType.GetEnumerableElementType();
IEnumerable seq = null;
if (elementType.IsNumeric())
{
seq = new NumberFormatter(culture, unit).Parse(value).CastTo(elementType);
}
else
{
var items = value.Split(new string[] { culture.NumberFormat.NumberGroupSeparator }, StringSplitOptions.RemoveEmptyEntries);
if (elementType.IsEnum)
{
seq = items.Select(item => Enum.Parse(elementType, item));
}
else
{
seq = items.Select(item => System.Convert.ChangeType(item, elementType));
}
}
Type genericBaseType = null;
if (targetType.IsArray)
{
elementType = targetType.GetElementType();
}
else if (targetType.IsGenericType)
{
genericBaseType = targetType.GetGenericTypeDefinition();
}
if (typeof(IEnumerable<>) == genericBaseType)
{
var genarg = targetType.GetGenericArguments().FirstOrDefault();
if (genarg != null && genarg.IsNumeric())
{
var comb = seq as ICombinedNumberSequence;
if (comb != null)
return comb.CastTo(genarg);
return seq.Cast