summaryrefslogtreecommitdiffstats
path: root/processing/src/main/java/com/yahoo/processing/request/properties/PropertyMap.java
blob: 66e49cff51ad5474f9ab0a37832d7d2dbfcb2cf8 (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
// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.processing.request.properties;

import com.yahoo.collections.MethodCache;
import com.yahoo.component.provider.FreezableClass;
import com.yahoo.processing.request.CompoundName;
import com.yahoo.processing.request.Properties;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;
import java.util.logging.Logger;

/**
 * A HashMap backing of Properties.
 * <p>
 * When this is cloned it will deep copy not only the model object map, but also each
 * clonable member inside the map.
 * <p>
 * Subclassing is supported, a hook can be implemented to provide conditional inclusion in the map.
 * By default - all properties are accepted, so set is never propagated.
 * <p>
 * This class is not multithread safe.
 *
 * @author bratseth
 */
public class PropertyMap extends Properties {

    private static Logger log = Logger.getLogger(PropertyMap.class.getName());
    private static final MethodCache cloneMethodCache = new MethodCache("clone");

    /**
     * The properties of this
     */
    private Map<CompoundName, Object> properties = new HashMap<>();

    public void set(CompoundName name, Object value, Map<String, String> context) {
        if (shouldSet(name, value))
            properties.put(name, value);
        else
            super.set(name, value, context);
    }

    /**
     * Return true if this value should be set in this map, false if the set should be propagated instead
     * This default implementation always returns true.
     */
    protected boolean shouldSet(CompoundName name, Object value) {
        return true;
    }

    public
    @Override
    Object get(CompoundName name, Map<String, String> context,
               com.yahoo.processing.request.Properties substitution) {
        if (!properties.containsKey(name)) return super.get(name, context, substitution);
        return properties.get(name);
    }

    public
    @Override
    PropertyMap clone() {
        PropertyMap clone = (PropertyMap) super.clone();
        clone.properties = cloneMap(this.properties);
        return clone;
    }

    /**
     * Clones a map by deep cloning each value which is cloneable and shallow copying all other values.
     */
    public static Map<CompoundName, Object> cloneMap(Map<CompoundName, Object> map) {
        Map<CompoundName, Object> cloneMap = new HashMap<>();
        for (Map.Entry<CompoundName, Object> entry : map.entrySet()) {
            Object cloneValue = clone(entry.getValue());
            if (cloneValue == null)
                cloneValue = entry.getValue(); // Shallow copy objects which does not support cloning
            cloneMap.put(entry.getKey(), cloneValue);
        }
        return cloneMap;
    }

    /**
     * Clones this object if it is clonable, and the clone is public. Returns null if not
     */
    public static Object clone(Object object) {
        if (object == null) return null;
        if (!(object instanceof Cloneable)) return null;
        if (object instanceof Object[])
            return arrayClone((Object[]) object);
        else
            return objectClone(object);
    }

    private static Object arrayClone(Object[] object) {
        Object[] arrayClone = Arrays.copyOf(object, object.length);
        // deep clone
        for (int i = 0; i < arrayClone.length; i++) {
            Object elementClone = clone(arrayClone[i]);
            if (elementClone != null)
                arrayClone[i] = elementClone;
        }
        return arrayClone;
    }

    private static Object objectClone(Object object) {
        // Fastpath for our own commonly used classes
        if (object instanceof FreezableClass) {
            // List common superclass of 'com.yahoo.search.result.Hit'
            return ((FreezableClass) object).clone();
        }
        else if (object instanceof PublicCloneable) {
            return ((PublicCloneable)object).clone();
        }
        else if (object instanceof LinkedList) { // TODO: Why? Somebody's infatuation with LinkedList knows no limits
            return ((LinkedList) object).clone();
        }
        else if (object instanceof ArrayList) { // TODO: Why? Likewise
            return ((ArrayList) object).clone();
        }

        try {
            Method cloneMethod = cloneMethodCache.get(object);
            if (cloneMethod == null) {
                log.warning("'" + object + "' is Cloneable, but has no clone method - will use the same instance in all requests");
                return null;
            }
            return cloneMethod.invoke(object);
        } catch (IllegalAccessException e) {
            log.warning("'" + object + "' is Cloneable, but clone method cannot be accessed - will use the same instance in all requests");
            return null;
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Exception cloning '" + object + "'", e);
        }
    }

    @Override
    public Map<String, Object> listProperties(CompoundName path, Map<String, String> context, Properties substitution) {
        Map<String, Object> map = super.listProperties(path, context, substitution);

        for (Map.Entry<CompoundName, Object> entry : properties.entrySet()) {
            if ( ! entry.getKey().hasPrefix(path)) continue;
            CompoundName propertyName = entry.getKey().rest(path.size());
            if (propertyName.isEmpty()) continue;
            map.put(propertyName.toString(), entry.getValue());
        }
        return map;
    }

}