OSDN Git Service

ArgumentExceptionのコンストラクタにparamNameが指定されていないか不正である箇所を修正 (CA2208)
[opentween/open-tween.git] / OpenTween / PostFilterRule.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2013 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 // 
5 // This file is part of OpenTween.
6 // 
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
10 // any later version.
11 // 
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 // for more details. 
16 // 
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
21
22 using System;
23 using System.Collections.Generic;
24 using System.Linq;
25 using System.Linq.Expressions;
26 using System.Text;
27 using System.Text.RegularExpressions;
28 using System.Xml.Serialization;
29
30 namespace OpenTween
31 {
32     /// <summary>
33     /// タブで使用する振り分けルールを表すクラス
34     /// </summary>
35     [XmlType("FiltersClass")]
36     public class PostFilterRule
37     {
38         /// <summary>
39         /// Compile() メソッドの呼び出しが必要な状態か否か
40         /// </summary>
41         [XmlIgnore]
42         public bool IsDirty { get; protected set; }
43
44         /// <summary>
45         /// コンパイルされた振り分けルール
46         /// </summary>
47         [XmlIgnore]
48         protected Func<PostClass, MyCommon.HITRESULT> FilterDelegate;
49
50         /// <summary>
51         /// 振り分けルールの概要
52         /// </summary>
53         [XmlIgnore]
54         public string SummaryText
55         {
56             get { return this.MakeSummary(); }
57         }
58
59         /// <summary>
60         /// ExecFilter() メソッドの実行時に自動でコンパイルを実行する
61         /// </summary>
62         /// <remarks>
63         /// テスト用途以外では AutoCompile に頼らず事前に Compile() メソッドを呼び出すこと
64         /// </remarks>
65         internal static bool AutoCompile { get; set; }
66
67         [XmlElement("NameFilter")]
68         public string FilterName
69         {
70             get { return this._FilterName; }
71             set
72             {
73                 this.IsDirty = true;
74                 this._FilterName = value;
75             }
76         }
77         private string _FilterName;
78
79         [XmlElement("ExNameFilter")]
80         public string ExFilterName
81         {
82             get { return this._ExFilterName; }
83             set
84             {
85                 this.IsDirty = true;
86                 this._ExFilterName = value;
87             }
88         }
89         private string _ExFilterName;
90
91         [XmlArray("BodyFilterArray")]
92         public string[] FilterBody
93         {
94             get { return this._FilterBody; }
95             set
96             {
97                 if (value == null)
98                     throw new ArgumentNullException("value");
99
100                 this.IsDirty = true;
101                 this._FilterBody = value;
102             }
103         }
104         private string[] _FilterBody = new string[0];
105
106         [XmlArray("ExBodyFilterArray")]
107         public string[] ExFilterBody
108         {
109             get { return this._ExFilterBody; }
110             set
111             {
112                 if (value == null)
113                     throw new ArgumentNullException("value");
114
115                 this.IsDirty = true;
116                 this._ExFilterBody = value;
117             }
118         }
119         private string[] _ExFilterBody = new string[0];
120
121         [XmlElement("SearchBoth")]
122         public bool UseNameField
123         {
124             get { return this._UseNameField; }
125             set
126             {
127                 this.IsDirty = true;
128                 this._UseNameField = value;
129             }
130         }
131         private bool _UseNameField;
132
133         [XmlElement("ExSearchBoth")]
134         public bool ExUseNameField
135         {
136             get { return this._ExUseNameField; }
137             set
138             {
139                 this.IsDirty = true;
140                 this._ExUseNameField = value;
141             }
142         }
143         private bool _ExUseNameField;
144
145         [XmlElement("MoveFrom")]
146         public bool MoveMatches
147         {
148             get { return this._MoveMatches; }
149             set
150             {
151                 this.IsDirty = true;
152                 this._MoveMatches = value;
153             }
154         }
155         private bool _MoveMatches;
156
157         [XmlElement("SetMark")]
158         public bool MarkMatches
159         {
160             get { return this._MarkMatches; }
161             set
162             {
163                 this.IsDirty = true;
164                 this._MarkMatches = value;
165             }
166         }
167         private bool _MarkMatches;
168
169         [XmlElement("SearchUrl")]
170         public bool FilterByUrl
171         {
172             get { return this._FilterByUrl; }
173             set
174             {
175                 this.IsDirty = true;
176                 this._FilterByUrl = value;
177             }
178         }
179         private bool _FilterByUrl;
180
181         [XmlElement("ExSearchUrl")]
182         public bool ExFilterByUrl
183         {
184             get { return this._ExFilterByUrl; }
185             set
186             {
187                 this.IsDirty = true;
188                 this._ExFilterByUrl = value;
189             }
190         }
191         private bool _ExFilterByUrl;
192
193         public bool CaseSensitive
194         {
195             get { return this._CaseSensitive; }
196             set
197             {
198                 this.IsDirty = true;
199                 this._CaseSensitive = value;
200             }
201         }
202         private bool _CaseSensitive;
203
204         public bool ExCaseSensitive
205         {
206             get { return this._ExCaseSensitive; }
207             set
208             {
209                 this.IsDirty = true;
210                 this._ExCaseSensitive = value;
211             }
212         }
213         private bool _ExCaseSensitive;
214
215         public bool UseLambda
216         {
217             get { return this._UseLambda; }
218             set
219             {
220                 this.IsDirty = true;
221                 this._UseLambda = value;
222             }
223         }
224         private bool _UseLambda;
225
226         public bool ExUseLambda
227         {
228             get { return this._ExUseLambda; }
229             set
230             {
231                 this.IsDirty = true;
232                 this._ExUseLambda = value;
233             }
234         }
235         private bool _ExUseLambda;
236
237         public bool UseRegex
238         {
239             get { return this._UseRegex; }
240             set
241             {
242                 this.IsDirty = true;
243                 this._UseRegex = value;
244             }
245         }
246         private bool _UseRegex;
247
248         public bool ExUseRegex
249         {
250             get { return this._ExUseRegex; }
251             set
252             {
253                 this.IsDirty = true;
254                 this._ExUseRegex = value;
255             }
256         }
257         private bool _ExUseRegex;
258
259         [XmlElement("IsRt")]
260         public bool FilterRt
261         {
262             get { return this._FilterRt; }
263             set
264             {
265                 this.IsDirty = true;
266                 this._FilterRt = value;
267             }
268         }
269         private bool _FilterRt;
270
271         [XmlElement("IsExRt")]
272         public bool ExFilterRt
273         {
274             get { return this._ExFilterRt; }
275             set
276             {
277                 this.IsDirty = true;
278                 this._ExFilterRt = value;
279             }
280         }
281         private bool _ExFilterRt;
282
283         [XmlElement("Source")]
284         public string FilterSource
285         {
286             get { return this._FilterSource; }
287             set
288             {
289                 this.IsDirty = true;
290                 this._FilterSource = value;
291             }
292         }
293         private string _FilterSource;
294
295         [XmlElement("ExSource")]
296         public string ExFilterSource
297         {
298             get { return this._ExFilterSource; }
299             set
300             {
301                 this.IsDirty = true;
302                 this._ExFilterSource = value;
303             }
304         }
305         private string _ExFilterSource;
306
307         public PostFilterRule()
308         {
309             this.IsDirty = true;
310
311             this.MarkMatches = true;
312             this.UseNameField = true;
313             this.ExUseNameField = true;
314         }
315
316         static PostFilterRule()
317         {
318             // TODO: TabsClass とかの改修が終わるまでデフォルト有効
319             PostFilterRule.AutoCompile = true;
320         }
321
322         /// <summary>
323         /// 振り分けルールをコンパイルします
324         /// </summary>
325         public void Compile()
326         {
327             var postParam = Expression.Parameter(typeof(PostClass), "x");
328
329             var matchExpr = this.MakeFiltersExpr(
330                 postParam,
331                 this.FilterName, this.FilterBody, this.FilterSource, this.FilterRt,
332                 this.UseRegex, this.CaseSensitive, this.UseNameField, this.UseLambda, this.FilterByUrl);
333
334             var excludeExpr = this.MakeFiltersExpr(
335                 postParam,
336                 this.ExFilterName, this.ExFilterBody, this.ExFilterSource, this.ExFilterRt,
337                 this.ExUseRegex, this.ExCaseSensitive, this.ExUseNameField, this.ExUseLambda, this.ExFilterByUrl);
338
339             Expression<Func<PostClass, MyCommon.HITRESULT>> filterExpr;
340
341             if (matchExpr != null)
342             {
343                 MyCommon.HITRESULT hitResult;
344
345                 if (this.MoveMatches)
346                     hitResult = MyCommon.HITRESULT.Move;
347                 else if (this.MarkMatches)
348                     hitResult = MyCommon.HITRESULT.CopyAndMark;
349                 else
350                     hitResult = MyCommon.HITRESULT.Copy;
351
352                 if (excludeExpr != null)
353                 {
354                     // x => matchExpr ? (!excludeExpr ? hitResult : None) : None
355                     filterExpr =
356                         Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
357                             Expression.Condition(
358                                 matchExpr,
359                                 Expression.Condition(
360                                     Expression.Not(excludeExpr),
361                                     Expression.Constant(hitResult),
362                                     Expression.Constant(MyCommon.HITRESULT.None)),
363                                 Expression.Constant(MyCommon.HITRESULT.None)),
364                             postParam);
365                 }
366                 else
367                 {
368                     // x => matchExpr ? hitResult : None
369                     filterExpr =
370                         Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
371                             Expression.Condition(
372                                 matchExpr,
373                                 Expression.Constant(hitResult),
374                                 Expression.Constant(MyCommon.HITRESULT.None)),
375                             postParam);
376                 }
377             }
378             else if (excludeExpr != null)
379             {
380                 // x => excludeExpr ? Exclude : None
381                 filterExpr =
382                     Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
383                         Expression.Condition(
384                             excludeExpr,
385                             Expression.Constant(MyCommon.HITRESULT.Exclude),
386                             Expression.Constant(MyCommon.HITRESULT.None)),
387                         postParam);
388             }
389             else // matchExpr == null && excludeExpr == null
390             {
391                 filterExpr = x => MyCommon.HITRESULT.None;
392             }
393
394             this.FilterDelegate = filterExpr.Compile();
395             this.IsDirty = false;
396         }
397
398         protected virtual Expression MakeFiltersExpr(
399             ParameterExpression postParam,
400             string filterName, string[] filterBody, string filterSource, bool filterRt,
401             bool useRegex, bool caseSensitive, bool useNameField, bool useLambda, bool filterByUrl)
402         {
403             var filterExprs = new List<Expression>();
404
405             if (useNameField && !string.IsNullOrEmpty(filterName))
406             {
407                 filterExprs.Add(Expression.OrElse(
408                     this.MakeGenericFilter(postParam, "ScreenName", filterName, useRegex, caseSensitive, exactMatch: true),
409                     this.MakeGenericFilter(postParam, "RetweetedBy", filterName, useRegex, caseSensitive, exactMatch: true)));
410             }
411             foreach (var body in filterBody)
412             {
413                 if (string.IsNullOrEmpty(body))
414                     continue;
415
416                 Expression bodyExpr;
417                 if (useLambda)
418                 {
419                     // TODO DynamicQuery相当のGPLv3互換なライブラリで置換する
420                     Expression<Func<PostClass, bool>> lambdaExpr = x => false;
421                     bodyExpr = lambdaExpr.Body;
422                 }
423                 else
424                 {
425                     if (filterByUrl)
426                         bodyExpr = this.MakeGenericFilter(postParam, "Text", body, useRegex, caseSensitive);
427                     else
428                         bodyExpr = this.MakeGenericFilter(postParam, "TextFromApi", body, useRegex, caseSensitive);
429
430                     // useNameField = false の場合は ScreenName と RetweetedBy も filterBody のマッチ対象となる
431                     if (!useNameField)
432                     {
433                         bodyExpr = Expression.OrElse(
434                             bodyExpr,
435                             this.MakeGenericFilter(postParam, "ScreenName", body, useRegex, caseSensitive, exactMatch: true));
436
437                         // bodyExpr || x.RetweetedBy != null && <MakeGenericFilter()>
438                         bodyExpr = Expression.OrElse(
439                             bodyExpr,
440                             Expression.AndAlso(
441                                 Expression.NotEqual(
442                                     Expression.Property(
443                                         postParam,
444                                         typeof(PostClass).GetProperty("RetweetedBy")),
445                                     Expression.Constant(null)),
446                                 this.MakeGenericFilter(postParam, "RetweetedBy", body, useRegex, caseSensitive, exactMatch: true)));
447                     }
448                 }
449
450                 filterExprs.Add(bodyExpr);
451             }
452             if (!string.IsNullOrEmpty(filterSource))
453             {
454                 if (filterByUrl)
455                     filterExprs.Add(this.MakeGenericFilter(postParam, "SourceHtml", filterSource, useRegex, caseSensitive));
456                 else
457                     filterExprs.Add(this.MakeGenericFilter(postParam, "Source", filterSource, useRegex, caseSensitive, exactMatch: true));
458             }
459             if (filterRt)
460             {
461                 // x.RetweetedId != null
462                 filterExprs.Add(Expression.NotEqual(
463                     Expression.Property(
464                         postParam,
465                         typeof(PostClass).GetProperty("RetweetedId")),
466                     Expression.Constant(null)));
467             }
468
469             if (filterExprs.Count == 0)
470             {
471                 return null;
472             }
473             else
474             {
475                 // filterExpr[0] && filterExpr[1] && ...
476                 var filterExpr = filterExprs[0];
477                 foreach (var expr in filterExprs.Skip(1))
478                 {
479                     filterExpr = Expression.AndAlso(filterExpr, expr);
480                 }
481
482                 return filterExpr;
483             }
484         }
485
486         protected Expression MakeGenericFilter(
487             ParameterExpression postParam, string targetFieldName, string pattern,
488             bool useRegex, bool caseSensitive, bool exactMatch = false)
489         {
490             // x.<targetFieldName>
491             var targetField = Expression.Property(
492                 postParam,
493                 typeof(PostClass).GetProperty(targetFieldName));
494
495             if (useRegex)
496             {
497                 var regex = new Regex(pattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
498
499                 // regex.IsMatch(targetField)
500                 return Expression.Call(
501                     Expression.Constant(regex),
502                     typeof(Regex).GetMethod("IsMatch", new[] { typeof(string) }),
503                     targetField);
504             }
505             else
506             {
507                 var compOpt = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
508
509                 if (exactMatch)
510                 {
511                     // 完全一致
512                     // pattern.Equals(targetField, compOpt)
513                     return Expression.Call(
514                         Expression.Constant(pattern),
515                         typeof(string).GetMethod("Equals", new[] { typeof(string), typeof(StringComparison) }),
516                         targetField,
517                         Expression.Constant(compOpt));
518
519                 }
520                 else
521                 {
522                     // 部分一致
523                     // targetField.IndexOf(pattern, compOpt) != -1
524                     return Expression.NotEqual(
525                         Expression.Call(
526                             targetField,
527                             typeof(string).GetMethod("IndexOf", new[] { typeof(string), typeof(StringComparison) }),
528                             Expression.Constant(pattern),
529                             Expression.Constant(compOpt)),
530                         Expression.Constant(-1));
531                 }
532             }
533         }
534
535         /// <summary>
536         /// ツイートの振り分けを実行する
537         /// </summary>
538         /// <param name="post">振り分けるツイート</param>
539         /// <returns>振り分け結果</returns>
540         public MyCommon.HITRESULT ExecFilter(PostClass post)
541         {
542             if (this.IsDirty)
543             {
544                 if (!PostFilterRule.AutoCompile)
545                     throw new InvalidOperationException("振り分け実行前に Compile() を呼び出す必要があります");
546
547                 this.Compile();
548             }
549
550             return this.FilterDelegate(post);
551         }
552
553         public PostFilterRule Clone()
554         {
555             return new PostFilterRule
556             {
557                 FilterBody = this.FilterBody,
558                 FilterName = this.FilterName,
559                 FilterSource = this.FilterSource,
560                 FilterRt = this.FilterRt,
561                 FilterByUrl = this.FilterByUrl,
562                 UseLambda = this.UseLambda,
563                 UseNameField = this.UseNameField,
564                 UseRegex = this.UseRegex,
565                 CaseSensitive = this.CaseSensitive,
566                 ExFilterBody = this.ExFilterBody,
567                 ExFilterName = this.ExFilterName,
568                 ExFilterSource = this.ExFilterSource,
569                 ExFilterRt = this.ExFilterRt,
570                 ExFilterByUrl = this.ExFilterByUrl,
571                 ExUseLambda = this.ExUseLambda,
572                 ExUseNameField = this.ExUseNameField,
573                 ExUseRegex = this.ExUseRegex,
574                 ExCaseSensitive = this.ExCaseSensitive,
575                 MoveMatches = this.MoveMatches,
576                 MarkMatches = this.MarkMatches,
577             };
578         }
579
580         /// <summary>
581         /// 振り分けルールの文字列表現を返します
582         /// </summary>
583         /// <returns>振り分けルールの概要</returns>
584         public override string ToString()
585         {
586             return this.SummaryText;
587         }
588
589         #region from Tween v1.1.0.0
590
591         // The code in this region block is based on code written by the following authors:
592         //   (C) 2007 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
593         //   (C) 2008 Moz (@syo68k)
594
595         /// <summary>
596         /// フィルタ一覧に表示する文言生成
597         /// </summary>
598         protected virtual string MakeSummary()
599         {
600             var fs = new StringBuilder();
601             if (!string.IsNullOrEmpty(this.FilterName) || this.FilterBody.Length > 0 || this.FilterRt || !string.IsNullOrEmpty(this.FilterSource))
602             {
603                 if (this.UseNameField)
604                 {
605                     if (!string.IsNullOrEmpty(this.FilterName))
606                     {
607                         fs.AppendFormat(Properties.Resources.SetFiltersText1, this.FilterName);
608                     }
609                     else
610                     {
611                         fs.Append(Properties.Resources.SetFiltersText2);
612                     }
613                 }
614                 if (this.FilterBody.Length > 0)
615                 {
616                     fs.Append(Properties.Resources.SetFiltersText3);
617                     foreach (var bf in this.FilterBody)
618                     {
619                         fs.Append(bf);
620                         fs.Append(" ");
621                     }
622                     fs.Length--;
623                     fs.Append(Properties.Resources.SetFiltersText4);
624                 }
625                 fs.Append("(");
626                 if (this.UseNameField)
627                 {
628                     fs.Append(Properties.Resources.SetFiltersText5);
629                 }
630                 else
631                 {
632                     fs.Append(Properties.Resources.SetFiltersText6);
633                 }
634                 if (this.UseRegex)
635                 {
636                     fs.Append(Properties.Resources.SetFiltersText7);
637                 }
638                 if (this.FilterByUrl)
639                 {
640                     fs.Append(Properties.Resources.SetFiltersText8);
641                 }
642                 if (this.CaseSensitive)
643                 {
644                     fs.Append(Properties.Resources.SetFiltersText13);
645                 }
646                 if (this.FilterRt)
647                 {
648                     fs.Append("RT/");
649                 }
650                 if (this.UseLambda)
651                 {
652                     fs.Append("LambdaExp/");
653                 }
654                 if (!string.IsNullOrEmpty(this.FilterSource))
655                 {
656                     fs.AppendFormat("Src…{0}/", this.FilterSource);
657                 }
658                 fs.Length--;
659                 fs.Append(")");
660             }
661             if (!string.IsNullOrEmpty(this.ExFilterName) || this.ExFilterBody.Length > 0 || this.ExFilterRt || !string.IsNullOrEmpty(this.ExFilterSource))
662             {
663                 //除外
664                 fs.Append(Properties.Resources.SetFiltersText12);
665                 if (this.ExUseNameField)
666                 {
667                     if (!string.IsNullOrEmpty(this.ExFilterName))
668                     {
669                         fs.AppendFormat(Properties.Resources.SetFiltersText1, this.ExFilterName);
670                     }
671                     else
672                     {
673                         fs.Append(Properties.Resources.SetFiltersText2);
674                     }
675                 }
676                 if (this.ExFilterBody.Length > 0)
677                 {
678                     fs.Append(Properties.Resources.SetFiltersText3);
679                     foreach (var bf in this.ExFilterBody)
680                     {
681                         fs.Append(bf);
682                         fs.Append(" ");
683                     }
684                     fs.Length--;
685                     fs.Append(Properties.Resources.SetFiltersText4);
686                 }
687                 fs.Append("(");
688                 if (this.ExUseNameField)
689                 {
690                     fs.Append(Properties.Resources.SetFiltersText5);
691                 }
692                 else
693                 {
694                     fs.Append(Properties.Resources.SetFiltersText6);
695                 }
696                 if (this.ExUseRegex)
697                 {
698                     fs.Append(Properties.Resources.SetFiltersText7);
699                 }
700                 if (this.ExFilterByUrl)
701                 {
702                     fs.Append(Properties.Resources.SetFiltersText8);
703                 }
704                 if (this.ExCaseSensitive)
705                 {
706                     fs.Append(Properties.Resources.SetFiltersText13);
707                 }
708                 if (this.ExFilterRt)
709                 {
710                     fs.Append("RT/");
711                 }
712                 if (this.ExUseLambda)
713                 {
714                     fs.Append("LambdaExp/");
715                 }
716                 if (!string.IsNullOrEmpty(this.ExFilterSource))
717                 {
718                     fs.AppendFormat("Src…{0}/", this.ExFilterSource);
719                 }
720                 fs.Length--;
721                 fs.Append(")");
722             }
723
724             fs.Append("(");
725             if (this.MoveMatches)
726             {
727                 fs.Append(Properties.Resources.SetFiltersText9);
728             }
729             else
730             {
731                 fs.Append(Properties.Resources.SetFiltersText11);
732             }
733             if (!this.MoveMatches && this.MarkMatches)
734             {
735                 fs.Append(Properties.Resources.SetFiltersText10);
736             }
737             else if (!this.MoveMatches)
738             {
739                 fs.Length--;
740             }
741
742             fs.Append(")");
743
744             return fs.ToString();
745         }
746         #endregion
747     }
748 }