KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > icl > saxon > expr > FunctionProxy


1 package com.icl.saxon.expr;
2 import com.icl.saxon.*;
3 import com.icl.saxon.om.Namespace;
4 import com.icl.saxon.om.NodeInfo;
5 import com.icl.saxon.om.NodeEnumeration;
6 import java.util.Vector;
7 import javax.xml.transform.TransformerException;
8 import java.lang.reflect.*;
9 import org.w3c.dom.*;
10 import org.w3c.xsl.XSLTContext;
11
12
13
14 /**
15 * This class acts as a proxy for an extension function defined as a method
16 * in a user-defined class
17 */

18
19 public class FunctionProxy extends Function {
20
21     private Class theClass;
22     private Vector candidateMethods = new Vector();
23     private XPathException theException = null;
24     private String name;
25     private Class resultClass = null;
26
27     /**
28     * Constructor: creates an uncommitted FunctionProxy
29     */

30
31     public FunctionProxy() {}
32
33     /**
34     * setFunctionName: locates the external class and the method (or candidate methods)
35     * to which this function relates. At this
36     * stage addArguments() will have normally been called, so the number of arguments is known. If no
37     * method of the required name is located, an exception is saved, but it is not thrown until
38     * an attempt is made to evaluate the function. All methods that match the required number of
39     * arguments are saved in a list (candidateMethods), a decision among these methods is made
40     * at run-time when the types of the actual arguments are known.<p>
41     * The method is also used while calling
42     * function-available(). In this case the number of arguments is not known (it will be
43     * set to zero). We return true if a match was found, regardless of the number of arguments.
44     * @param className The Java class name
45     * @param localName The local name used in the XPath function call
46     * @return true if the function is available (with some number of arguments).
47     */

48
49     public boolean setFunctionName(Class reqClass, String localName) {
50         boolean isAvailable = false;
51         // System.err.println("FunctionProxy.setMethod(" + reqClass + ":" + localName);
52

53         name = localName;
54         int numArgs = getNumberOfArguments();
55         int significantArgs = numArgs;
56
57         theClass = reqClass;
58
59         // if the method name is "new", look for a matching constructor
60

61         if (name.equals("new")) {
62             
63             int mod = theClass.getModifiers();
64             if (Modifier.isAbstract(mod)) {
65                 theException = new XPathException("Class " + theClass + " is abstract");
66                 return false;
67             }
68             if (Modifier.isInterface(mod)) {
69                 theException = new XPathException(theClass + " is an interface");
70                 return false;
71             }
72             if (Modifier.isPrivate(mod)) {
73                 theException = new XPathException("Class " + theClass + " is private");
74                 return false;
75             }
76             if (Modifier.isProtected(mod)) {
77                 theException = new XPathException("Class " + theClass + " is protected");
78                 return false;
79             }
80
81             resultClass = theClass;
82             Constructor[] constructors = theClass.getConstructors();
83             for (int c=0; c<constructors.length; c++) {
84                 isAvailable = true;
85                 Constructor theConstructor = constructors[c];
86                 if (theConstructor.getParameterTypes().length == numArgs) {
87                     isAvailable = true;
88                     candidateMethods.addElement(theConstructor);
89                 }
90             }
91             if (isAvailable) {
92                 return true;
93             } else {
94                 theException = new XPathException("No constructor with " + numArgs +
95                                  (numArgs==1 ? " parameter" : " parameters") +
96                                   " found in class " + theClass.getName());
97                 return false;
98             }
99         } else {
100             
101             // convert any hyphens in the name, camelCasing the following character
102

103             StringBuffer buff = new StringBuffer();
104             boolean afterHyphen = false;
105             for (int n=0; n<name.length(); n++) {
106                 char c = name.charAt(n);
107                 if (c=='-') {
108                     afterHyphen = true;
109                 } else {
110                     if (afterHyphen) {
111                         buff.append(Character.toUpperCase(c));
112                     } else {
113                         buff.append(c);
114                     }
115                     afterHyphen = false;
116                 }
117             }
118             
119             name = buff.toString();
120             
121             // special case for saxon:if() (Java reserved word)
122

123             if (name.equals("if")) {
124                 name = "IF";
125             }
126     
127             // look through the methods of this class to find one that matches the local name
128

129             Method[] methods = theClass.getMethods();
130             boolean consistentReturnType = true;
131             for (int m=0; m<methods.length; m++) {
132
133                 Method theMethod = methods[m];
134                 
135                 if (theMethod.getName().equals(name) &&
136                         Modifier.isPublic(theMethod.getModifiers())) {
137                     isAvailable = true;
138                     if (consistentReturnType) {
139                         if (resultClass==null) {
140                             resultClass = theMethod.getReturnType();
141                         } else {
142                             consistentReturnType =
143                                 (theMethod.getReturnType()==resultClass);
144                         }
145                     }
146                     Class[] theParameterTypes = theMethod.getParameterTypes();
147                     boolean isStatic = Modifier.isStatic(theMethod.getModifiers());
148
149                     // if the method is not static, the first supplied argument is the instance, so
150
// discount it
151

152                     significantArgs = (isStatic ? numArgs : numArgs - 1);
153
154                     if (significantArgs>=0) {
155
156                         //System.err.println("Looking for " + name + "(" + significantArgs +"), trying " +
157
// methods[m].getName() + "(" + theParameterTypes.length + ")");
158

159                         if (theParameterTypes.length == significantArgs &&
160                                 (significantArgs==0 || theParameterTypes[0]!=Context.class))
161                                 // TODO: ad XSLTContext.class
162
{
163                             isAvailable = true;
164                             candidateMethods.addElement(theMethod);
165                         }
166             
167                         // we allow the method to have an extra parameter if the first parameter is Context
168

169                         if (theParameterTypes.length == significantArgs+1 &&
170                                 (theParameterTypes[0]==Context.class ||
171                                  theParameterTypes[0]==XSLTContext.class)) {
172                             isAvailable = true;
173                             candidateMethods.addElement(theMethod);
174                         }
175                     }
176                 }
177             }
178             
179             if (!consistentReturnType) {
180                 resultClass = null; // different return type from different methods
181
}
182     
183             // No method found?
184

185             if (isAvailable) {
186                 return true;
187             } else {
188                 theException = new XPathException("No method matching " + name +
189                                      " with " + significantArgs +
190                                      (significantArgs==1 ? " parameter" : " parameters") +
191                                       " found in class " + theClass.getName());
192                 return false;
193             }
194         }
195
196     }
197
198     /**
199     * Determine the data type of the expression, if possible
200     * @return Value.ANY (meaning not known in advance)
201     */

202
203     public int getDataType() {
204         if (resultClass==null || resultClass==Value.class) {
205             return Value.ANY;
206         } else if (resultClass.toString().equals("void")) {
207             return Value.NODESET;
208         } else if (resultClass==String.class || resultClass==StringValue.class) {
209             return Value.STRING;
210         } else if (resultClass==Boolean.class || resultClass==boolean.class ||
211                     resultClass==BooleanValue.class) {
212             return Value.BOOLEAN;
213         } else if (resultClass==Double.class || resultClass==double.class ||
214                     resultClass==Float.class || resultClass==float.class ||
215                     resultClass==Long.class || resultClass==long.class ||
216                     resultClass==Integer.class || resultClass==int.class ||
217                     resultClass==Short.class || resultClass==short.class ||
218                     resultClass==Byte.class || resultClass==byte.class ||
219                     resultClass==NumericValue.class) {
220             return Value.NUMBER;
221         } else if (NodeSetValue.class.isAssignableFrom(resultClass) ||
222                     NodeEnumeration.class.isAssignableFrom(resultClass) ||
223                     NodeList.class.isAssignableFrom(resultClass) ||
224                     Node.class.isAssignableFrom(resultClass)) {
225             return Value.NODESET;
226         } else {
227             return Value.OBJECT;
228         }
229     }
230             
231     /**
232     * Get the name of the function
233     */

234
235     public String getName() {
236         return name;
237     }
238
239     /**
240     * Simplify the function (by simplifying its arguments)
241     */

242
243     public Expression simplify() throws XPathException {
244         for (int i=0; i<getNumberOfArguments(); i++) {
245             argument[i] = argument[i].simplify();
246         }
247         
248         // if the data type of all arguments is known at compile time,
249
// we can choose the method now
250

251         if (candidateMethods.size() > 1) {
252             boolean allKnown = true;
253             for (int i=0; i<getNumberOfArguments(); i++) {
254                 int type = argument[i].getDataType();
255                 if (type==Value.ANY || type==Value.OBJECT) {
256                     allKnown = false;
257                     break;
258                 }
259             }
260             if (allKnown) {
261                 // set up some dummy arguments: only the data type matters
262
Value[] argValues = new Value[getNumberOfArguments()];
263                 for (int k=0; k<getNumberOfArguments(); k++) {
264                     switch (argument[k].getDataType()) {
265                         case Value.BOOLEAN:
266                             argValues[k] = new BooleanValue(true);
267                             break;
268                         case Value.NUMBER:
269                             argValues[k] = new NumericValue(1.0);
270                             break;
271                         case Value.STRING:
272                             argValues[k] = new StringValue("");
273                             break;
274                         case Value.NODESET:
275                             argValues[k] = new EmptyNodeSet();
276                             break;
277                     }
278                 }
279                 try {
280                     Object method = getBestFit(argValues);
281                     candidateMethods = new Vector();
282                     candidateMethods.addElement(method);
283                 } catch (XPathException err) {
284                     theException = err;
285                 }
286             }
287         }
288         return this;
289     }
290
291     /**
292     * Determine which aspects of the context the expression depends on. The result is
293     * a bitwise-or'ed value composed from constants such as Context.VARIABLES and
294     * Context.CURRENT_NODE
295     */

296
297     public int getDependencies() {
298         int dep = 0;
299         //if (usesContext) {
300
dep = Context.CONTEXT_NODE | Context.POSITION | Context.LAST;
301         //}
302
for (int i=0; i<getNumberOfArguments(); i++) {
303             dep |= argument[i].getDependencies();
304         }
305         return dep;
306     }
307
308     /**
309     * Perform a partial evaluation of the expression, by eliminating specified dependencies
310     * on the context.
311     * @param dependencies The dependencies to be removed
312     * @param context The context to be used for the partial evaluation
313     * @return a new expression that does not have any of the specified
314     * dependencies
315     */

316
317     public Expression reduce(int dependencies, Context context) throws XPathException {
318
319         // only safe thing is to evaluate it now
320
if (//usesContext &&
321
(dependencies & (Context.CONTEXT_NODE |
322                                  Context.POSITION | Context.LAST)) != 0) {
323             return evaluate(context);
324             
325         } else {
326
327             FunctionProxy fp = new FunctionProxy();
328             fp.theClass = theClass;
329             fp.candidateMethods = candidateMethods;
330             fp.theException = theException;
331             fp.name = name;
332             fp.argument = new Expression[getNumberOfArguments()];
333             for (int a=0; a<getNumberOfArguments(); a++) {
334                 fp.addArgument(argument[a].reduce(dependencies, context));
335             }
336             return fp;
337         }
338     }
339         
340     /**
341     * Get the best fit amongst all the candidate methods or constructors
342     * @return the result is either a Method or a Constructor. In JDK 1.2 these
343     * have a common superclass, AccessibleObject, but this is not available
344     * in JDK 1.1, which we still support.
345     */

346     
347     public Object getBestFit(Value[] argValues) throws XPathException {
348
349         if (candidateMethods.size() == 1) {
350             // short cut: there is only one candidate method
351
return candidateMethods.elementAt(0);
352             
353         } else {
354             // choose the best fit method or constructor
355
// for each pair of candidate methods, eliminate either or both of the pair
356
// if one argument is less-preferred
357
int candidates = candidateMethods.size();
358             boolean eliminated[] = new boolean[candidates];
359             for (int i=0; i<candidates; i++) {
360                 eliminated[i] = false;
361             }
362                        
363             for (int i=0; i<candidates-1; i++) {
364                 int[] pref_i = getConversionPreferences(
365                                     argValues,
366                                     candidateMethods.elementAt(i));
367                 if (!eliminated[i]) {
368                     for (int j=i+1; j<candidates; j++) {
369                         if (!eliminated[j]) {
370                             int[] pref_j = getConversionPreferences(
371                                             argValues,
372                                             candidateMethods.elementAt(j));
373                             for (int k=0; k<pref_j.length; k++) {
374                                 if (pref_i[k] > pref_j[k]) { // high number means less preferred
375
eliminated[i] = true;
376                                 }
377                                 if (pref_i[k] < pref_j[k]) {
378                                     eliminated[j] = true;
379                                 }
380                             }
381                         }
382                     }
383                 }
384             }
385             
386             int remaining = 0;
387             Object theMethod = null; // could be AccessibleObject in JDK 1.2
388
for (int r=0; r<candidates; r++) {
389                 if (!eliminated[r]) {
390                     theMethod = candidateMethods.elementAt(r);
391                     remaining++;
392                 }
393             }
394             
395             if (remaining==0) {
396                 throw new XPathException("There is no Java method that is a unique best match");
397             }
398
399             if (remaining>1) {
400                 throw new XPathException("There are several Java methods that match equally well");
401             }
402             
403             return theMethod;
404         }
405     }
406             
407     /**
408     * Evaluate the function. <br>
409     * @param context The context in which the function is to be evaluated
410     * @return a Value representing the result of the function.
411     * @throws XPathException if the function cannot be evaluated.
412     */

413
414     public Value evaluate(Context context) throws XPathException {
415         Object result = call(context);
416         return convertJavaObjectToXPath(result, context.getController());
417     }
418     
419     public String evaluateAsString(Context context) throws XPathException {
420         if (resultClass==String.class) {
421             return (String)call(context);
422         } else if (resultClass==NodeEnumeration.class) {
423             NodeEnumeration enum = enumerate(context, true);
424             if (enum.hasMoreElements()) {
425                 return enum.nextElement().getStringValue();
426             } else {
427                 return "";
428             }
429         } else {
430             return evaluate(context).asString();
431         }
432     }
433
434     public double evaluateAsNumber(Context context) throws XPathException {
435         if (resultClass==double.class) {
436             return ((Double)call(context)).doubleValue();
437         } else if (resultClass==NodeEnumeration.class) {
438             NodeEnumeration enum = enumerate(context, true);
439             if (enum.hasMoreElements()) {
440                 return Value.stringToNumber(enum.nextElement().getStringValue());
441             } else {
442                 return Double.NaN;
443             }
444         } else {
445             return evaluate(context).asNumber();
446         }
447     }
448
449     public boolean evaluateAsBoolean(Context context) throws XPathException {
450         if (resultClass==boolean.class) {
451             return ((Boolean)call(context)).booleanValue();
452         } else if (resultClass==NodeEnumeration.class) {
453             NodeEnumeration enum = enumerate(context, false);
454             return enum.hasMoreElements();
455         } else {
456             return evaluate(context).asBoolean();
457         }
458     }
459
460     public NodeEnumeration enumerate(Context context, boolean requireSorted) throws XPathException {
461         if (resultClass==NodeEnumeration.class) {
462             NodeEnumeration result = (NodeEnumeration)call(context);
463             if (requireSorted && !result.isSorted()) {
464                 NodeSetExtent extent = new NodeSetExtent(result, context.getController());
465                 extent.sort();
466                 return extent.enumerate();
467             } else {
468                 return result;
469             }
470         } else {
471             return super.enumerate(context, requireSorted);
472         }
473     }
474
475     /**
476     * Call the external function and return its result as a Java object
477     */

478     
479     private Object call(Context context) throws XPathException {
480
481         // Fail now if no method was found
482

483         if (theException!=null) {
484             throw theException;
485         }
486         context.setException(null);
487
488         Value[] argValues = new Value[getNumberOfArguments()];
489         for (int a=0; a<getNumberOfArguments(); a++) {
490             argValues[a] = argument[a].evaluate(context);
491         }
492         
493         // find the best fit method
494

495         Object theMethod = getBestFit(argValues);
496                             // could be an AccessibleObject in JDK 1.2
497

498         // now call it
499

500         Class[] theParameterTypes;
501         
502         if (theMethod instanceof Constructor) {
503             Constructor constructor = (Constructor)theMethod;
504             theParameterTypes = constructor.getParameterTypes();
505             Object[] params = new Object[theParameterTypes.length];
506             
507             setupParams(argValues, params, theParameterTypes, 0, 0);
508
509             try {
510                 Object obj = constructor.newInstance(params);
511                 if (context.getException() != null) {
512                     throw context.getException();
513                 }
514                 return obj;
515                 //return new ObjectValue(obj);
516
} catch (InstantiationException err0) {
517                 throw new XPathException ("Cannot instantiate class", err0);
518             } catch (IllegalAccessException err1) {
519                 throw new XPathException ("Constructor access is illegal", err1);
520             } catch (IllegalArgumentException err2) {
521                 throw new XPathException ("Argument is of wrong type", err2);
522             } catch (InvocationTargetException err3) {
523                 Throwable ex = err3.getTargetException();
524                 if (ex instanceof XPathException) {
525                     throw (XPathException)ex;
526                 } else {
527                     if (context.getController().isTracing()) {
528                         err3.getTargetException().printStackTrace();
529                     }
530                     throw new XPathException ("Exception in extension function " +
531                                             err3.getTargetException().toString());
532                 }
533             }
534         } else {
535             Method method = (Method)theMethod;
536             boolean isStatic = Modifier.isStatic(method.getModifiers());
537             Object theInstance;
538             theParameterTypes = method.getParameterTypes();
539             boolean usesContext = theParameterTypes.length > 0 &&
540                                   (theParameterTypes[0] == Context.class ||
541                                    theParameterTypes[0] == XSLTContext.class);
542             if (isStatic) {
543                 theInstance = null;
544             } else {
545                 int actualArgs = getNumberOfArguments();
546                 if (actualArgs==0) {
547                     throw new XPathException("Must supply an argument for an instance-level extension function");
548                 }
549                 Value arg0 = argument[0].evaluate(context);
550                 if (arg0 instanceof ObjectValue) {
551                     // TODO: check it's the right type for this method
552
theInstance = ((ObjectValue)arg0).getObject();
553                 } else if (theClass==String.class) {
554                     theInstance = arg0.asString();
555                 } else if (theClass==Boolean.class) {
556                     theInstance = new Boolean(arg0.asBoolean());
557                 } else if (theClass==Double.class) {
558                     theInstance = new Double(arg0.asNumber());
559                 } else {
560                     throw new XPathException("First argument is not an object instance");
561                 }
562                 
563             }
564
565             int requireArgs = theParameterTypes.length -
566                                  (usesContext ? 1 : 0) +
567                                  (isStatic ? 0 : 1);
568                                  
569             checkArgumentCount(requireArgs, requireArgs);
570             Object[] params = new Object[theParameterTypes.length];
571
572             if (usesContext) {
573                 params[0] = context;
574             }
575
576             setupParams(argValues, params, theParameterTypes,
577                             (usesContext ? 1 : 0),
578                             (isStatic ? 0 : 1)
579                         );
580
581             try {
582                 Object result = method.invoke(theInstance, params);
583                 if (context.getException() != null) {
584                     throw context.getException();
585                 }
586                 if (method.getReturnType().toString().equals("void")) {
587                     // method returns void:
588
// tried (method.getReturnType()==Void.class) unsuccessfully
589
return new EmptyNodeSet();
590                 }
591                 return result;
592
593             } catch (IllegalAccessException err1) {
594                 throw new XPathException ("Method access is illegal", err1);
595             } catch (IllegalArgumentException err2) {
596                 throw new XPathException ("Argument is of wrong type", err2);
597             } catch (InvocationTargetException err3) {
598                 Throwable ex = err3.getTargetException();
599                 if (ex instanceof XPathException) {
600                     throw (XPathException)ex;
601                 } else {
602                     if (context.getController().isTracing()) {
603                         err3.getTargetException().printStackTrace();
604                     }
605                     throw new XPathException ("Exception in extension function " +
606                                             err3.getTargetException().toString());
607                 }
608             }
609         }
610     }
611
612     /**
613     * Convert a Java object to an XPath value. This method is called to handle the result
614     * of an external function call (but only if the required type is not known),
615     * and also to process global parameters passed to the stylesheet.
616     */

617
618     public static Value convertJavaObjectToXPath(Object result, Controller controller)
619                                           throws XPathException {
620         if (result==null) {
621             return new ObjectValue(null);
622
623         } else if (result instanceof String) {
624             return new StringValue((String)result);
625             
626         } else if (result instanceof Boolean) {
627             return new BooleanValue(((Boolean)result).booleanValue());
628
629         } else if (result instanceof Double) {
630             return new NumericValue(((Double)result).doubleValue());
631         } else if (result instanceof Float) {
632             return new NumericValue((double)((Float)result).floatValue());
633         } else if (result instanceof Short) {
634             return new NumericValue((double)((Short)result).shortValue());
635         } else if (result instanceof Integer) {
636             return new NumericValue((double)((Integer)result).intValue());
637         } else if (result instanceof Long) {
638             return new NumericValue((double)((Long)result).longValue());
639         } else if (result instanceof Character) {
640             return new NumericValue((double)((Character)result).charValue());
641         } else if (result instanceof Byte) {
642             return new NumericValue((double)((Byte)result).byteValue());
643
644         } else if (result instanceof Value) {
645             return (Value)result;
646             
647         } else if (result instanceof NodeInfo) {
648             return new SingletonNodeSet((NodeInfo)result);
649         } else if (result instanceof NodeEnumeration) {
650             // TODO: should avoid breaking the pipeline at this point.
651
// It's only necessary because a NodeEnumeration isn't a Value.
652
return new NodeSetExtent((NodeEnumeration)result,
653                                       controller);
654         } else if (result instanceof org.w3c.dom.NodeList) {
655             NodeList list = ((NodeList)result);
656             NodeInfo[] nodes = new NodeInfo[list.getLength()];
657             for (int i=0; i<list.getLength(); i++) {
658                 if (list.item(i) instanceof NodeInfo) {
659                     nodes[i] = (NodeInfo)list.item(i);
660                 } else {
661                     throw new XPathException("Supplied NodeList contains non-Saxon DOM Nodes");
662                 }
663
664             }
665             return new NodeSetExtent(nodes, controller);
666         } else if (result instanceof org.w3c.dom.Node) {
667             throw new XPathException("Result is a non-Saxon DOM Node");
668         } else {
669             return new ObjectValue(result);
670         }
671     }
672
673     /**
674     * Get an array of integers representing the conversion distances of each "real" argument
675     * to a given method
676     * @param argValues: the actual argumetn values supplied
677     * @param method: the method or constructor. (Could be an AccessibleObject in JDK 1.2)
678     * @return an array of integers, one for each argument, indicating the conversion
679     * distances. A high number indicates low preference.
680     */

681
682     private int[] getConversionPreferences(Value[] argValues, Object method) {
683
684         Class[] params;
685         int firstArg;
686         
687         if (method instanceof Constructor) {
688             firstArg = 0;
689             params = ((Constructor)method).getParameterTypes();
690         } else {
691             boolean isStatic = Modifier.isStatic(((Method)method).getModifiers());
692             firstArg = (isStatic ? 0 : 1);
693             params = ((Method)method).getParameterTypes();
694         }
695         
696         int noOfArgs = getNumberOfArguments() - firstArg;
697         int preferences[] = new int[noOfArgs];
698         int firstParam = 0;
699         
700         if (params[0] == Context.class || params[0] == XSLTContext.class) {
701             firstParam = 1;
702         }
703
704         for (int i = 0; i<noOfArgs; i++) {
705             preferences[i] = argValues[i+firstArg].conversionPreference(params[i+firstParam]);
706         }
707         
708         return preferences;
709     }
710           
711                 
712     private void setupParams(Value[] argValues,
713                              Object[] params,
714                              Class[] paramTypes,
715                              int firstParam,
716                              int firstArg) throws XPathException {
717         int j=firstParam;
718         for (int i=firstArg; i<getNumberOfArguments(); i++) {
719             params[j] = argValues[i].convertToJava(paramTypes[j]);
720             j++;
721         }
722     }
723
724 }
725
726 //
727
// The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
728
// you may not use this file except in compliance with the License. You may obtain a copy of the
729
// License at http://www.mozilla.org/MPL/
730
//
731
// Software distributed under the License is distributed on an "AS IS" basis,
732
// WITHOUT WARRANTY OF ANY KIND, either express or implied.
733
// See the License for the specific language governing rights and limitations under the License.
734
//
735
// The Original Code is: all this file.
736
//
737
// The Initial Developer of the Original Code is
738
// Michael Kay of International Computers Limited (mhkay@iclway.co.uk).
739
//
740
// Portions created by (your name) are Copyright (C) (your legal entity). All Rights Reserved.
741
//
742
// Contributor(s): none.
743
//
744
Popular Tags