aboutsummaryrefslogtreecommitdiffstats
path: root/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/Reference.java
blob: e5559bfee7aa74c01250c27673be012d2245e504 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.searchlib.rankingexpression;

import com.yahoo.searchlib.rankingexpression.rule.Arguments;
import com.yahoo.searchlib.rankingexpression.rule.CompositeNode;
import com.yahoo.searchlib.rankingexpression.rule.ExpressionNode;
import com.yahoo.searchlib.rankingexpression.rule.NameNode;
import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode;
import com.yahoo.searchlib.rankingexpression.rule.SerializationContext;
import com.yahoo.tensor.evaluation.Name;

import java.util.Deque;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;

/**
 * A reference to a feature, function, or value in ranking expressions
 *
 * @author bratseth
 */
public class Reference extends Name implements Comparable<Reference> {

    private final int hashCode;

    private final Arguments arguments;

    /** The output, or null if none */
    private final String output;

    /** True if this was created by the "fromIdentifier" method. This lets us separate 'foo()' and 'foo' */
    private final boolean isIdentifier;

    private final static Pattern identifierPattern = Pattern.compile("[A-Za-z0-9_@.\"$-]+");

    private static boolean isValidIdentifier(String name) {
        return identifierPattern.matcher(name).matches();
    }

    public Reference(String name, Arguments arguments, String output) {
        this(name, arguments, output, false);
    }

    private Reference(String name, Arguments arguments, String output, boolean isIdentifier) {
        super(name);
        Objects.requireNonNull(name, "name cannot be null");
        Objects.requireNonNull(arguments, "arguments cannot be null");
        this.arguments = arguments;
        this.output = output;
        this.hashCode = Objects.hash(name(), arguments, output, isIdentifier);
        this.isIdentifier = isIdentifier;
    }

    public Arguments arguments() { return arguments; }

    public String output() { return output; }

    /** Returns true if this was created by fromIdentifier. Identifiers have no arguments or outputs. */
    public boolean isIdentifier() { return isIdentifier; }

    /**
     * A <i>simple feature reference</i> is a reference with a single identifier argument
     * (and an optional output).
     */
    public boolean isSimple() {
        return simpleArgument().isPresent();
    }

    /**
     * If the arguments of this contains a single argument which is an identifier, it is returned.
     * Otherwise null is returned.
     */
    public Optional<String> simpleArgument() {
        if (arguments.expressions().size() != 1) return Optional.empty();
        ExpressionNode argument = arguments.expressions().get(0);

        if (argument instanceof ReferenceNode) {
            ReferenceNode refArgument = (ReferenceNode) argument;

            if ( ! refArgument.reference().isIdentifier()) return Optional.empty();

            return Optional.of(refArgument.getName());
        }
        else if (argument instanceof NameNode) {
            return Optional.of(((NameNode) argument).getValue());
        }
        else {
            return Optional.empty();
        }
    }

    public Reference withArguments(Arguments arguments) {
        return new Reference(name(), arguments, output, isIdentifier && arguments.isEmpty());
    }

    public Reference withOutput(String output) {
        return new Reference(name(), arguments, output, isIdentifier && output == null);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (o.hashCode() != this.hashCode()) return false; // because this has a fast hashCode
        if ( ! (o instanceof Reference)) return false;
        Reference other = (Reference) o;
        if (!Objects.equals(other.name(), this.name())) return false;
        if (!Objects.equals(other.arguments, this.arguments)) return false;
        if (!Objects.equals(other.output, this.output)) return false;
        if (!Objects.equals(other.isIdentifier, this.isIdentifier)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    @Override
    public String toString() {
        return toString(new StringBuilder(), new SerializationContext(), null, null).toString();
    }

    public static final String RANKING_EXPRESSION_WRAPPER = "rankingExpression";

    public static String wrapInRankingExpression(String name) {
        return RANKING_EXPRESSION_WRAPPER + "(" + name + ")";
    }

    public boolean isSimpleRankingExpressionWrapper() {
        return name().equals(RANKING_EXPRESSION_WRAPPER) && output == null && isSimple();
    }

    public StringBuilder toString(StringBuilder b, SerializationContext context, Deque<String> path, CompositeNode parent) {
        b.append(name());
        if (arguments.expressions().size() > 0) {
            b.append("(");
            if (isSimpleRankingExpressionWrapper()) {
                b.append(simpleArgument().get());
            } else {
                for (int i = 0; i < arguments.expressions().size(); i++) {
                    ExpressionNode e = arguments.expressions().get(i);
                    e.toString(b, context, path, parent);
                    if (i+1 < arguments.expressions().size()) {
                        b.append(',');
                    }
                }
            }
            b.append(")");
        }
        if (output != null)
            b.append(".").append(output);
        return b;
    }

    @Override
    public int compareTo(Reference o) {
        return this.toString().compareTo(o.toString());
    }

    /** Creates a reference from a simple identifier. */
    public static Reference fromIdentifier(String identifier) {
        if ( ! isValidIdentifier(identifier))
            throw new IllegalArgumentException("Identifiers can only contain [A-Za-z0-9_@.\"$-]+, but was '" + identifier + "'");
        return new Reference(identifier, Arguments.EMPTY, null, true);
    }

    /**
     * Creates a reference to a simple feature consisting of a name and a single argument
     */
    public static Reference simple(String name, String argumentValue) {
        return new Reference(name, new Arguments(new ReferenceNode(argumentValue)), null);
    }

    /**
     * Returns the given simple feature as a reference, or empty if it is not a valid simple
     * feature string on the form name(argument) or name(argument).outputname
     */
    public static Optional<Reference> simple(String feature) {
        int startParenthesis = feature.indexOf('(');
        if (startParenthesis < 1)
            return Optional.empty();
        int endParenthesis = feature.lastIndexOf(')');
        String featureName = feature.substring(0, startParenthesis);
        if (endParenthesis < startParenthesis)
            return Optional.empty();
        String argument = feature.substring(startParenthesis + 1, endParenthesis);
        if (argument.startsWith("'") || argument.startsWith("\""))
            argument = argument.substring(1);
        if (argument.endsWith("'") || argument.endsWith("\""))
            argument = argument.substring(0, argument.length() - 1);
        if ( ! isValidIdentifier(argument))
            return Optional.empty();
        Reference result = simple(featureName, argument);
        if (endParenthesis + 1 == feature.length())
            return Optional.of(result);
        // text after end parenthesis; must be ".outputname"
        int lastDot = feature.lastIndexOf('.');
        if (endParenthesis + 1 == lastDot && lastDot + 1 < feature.length()) {
            String outputName = feature.substring(lastDot + 1);
            if (isValidIdentifier(outputName)) {
                result = result.withOutput(outputName);
                return Optional.of(result);
            }
        }
        return Optional.empty();
    }

}