All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vectorprint.configuration.Settings Maven / Gradle / Ivy

Go to download

This project is about configuration of applications and about parameterization of Objects. This library offers annotations (and annotation processors), parsers, typing, observing changes, serialization, cloning and more when working with settings and/or object parameters. Settings and its features can be declared using an xml format. The library offers syntax support for settings and parameters in a loosely coupled manner. You are not restricted to built in syntax, you can provide your own. At runtime this library tracks keys for which a default is used because they are not found in settings. Also it tracks unused keys. You can stack features for settings such as caching, preparing keys and values, readonlyness, threadsafety, helpsupport, reading / parsing from input. You can easily develop your own features for settings.

There is a newer version: 12.2
Show newest version
package com.vectorprint.configuration;

/*
 * #%L
 * VectorPrintConfig3.0
 * %%
 * Copyright (C) 2011 - 2013 VectorPrint
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
import com.vectorprint.VectorPrintRuntimeException;
import com.vectorprint.configuration.annotation.Setting;
import com.vectorprint.configuration.annotation.SettingsAnnotationProcessor;
import com.vectorprint.configuration.annotation.SettingsField;
import com.vectorprint.configuration.binding.AbstractBindingHelperDecorator;
import com.vectorprint.configuration.binding.settings.EnhancedMapBindingFactory;
import com.vectorprint.configuration.binding.settings.SettingsBindingService;
import com.vectorprint.configuration.decoration.AbstractPropertiesDecorator;
import com.vectorprint.configuration.decoration.DecorationAware;
import java.awt.Color;
import java.io.File;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
 * Enhances Java Map with support for data types, debugging info, working with
 * default values in code. Internally a backing Map is used which is a HashMap
 * by default. You can provide your own backing map in {@link #Settings(java.util.Map)
 * }, this may not be an EnhancedMap implementation. You cannot subclass this
 * class, instead subclass {@link AbstractPropertiesDecorator} and wrap an
 * instance of this class.
 *
 * @see EnhancedMapBindingFactory
 * @see com.vectorprint.configuration.decoration
 * @see SettingsAnnotationProcessor
 * @see SettingsField
 * @see Setting
 *
 * @author Eduard Drenth at VectorPrint.nl
 */
public final class Settings implements EnhancedMap, DecorationAware {

    private static final long serialVersionUID = 1;
    private static final Logger log = Logger.getLogger(Settings.class.getName());
    private final Map backingMap;

    @Override
    public void listProperties(PrintStream ps) {
        ps.println("settings with id " + getId() + ":");
        ps.println();
        backingMap.entrySet().forEach((entry) -> {
            ps.println(entry.getKey() + "=" + (entry.getValue() != null ? Arrays.asList(entry.getValue()) : ""));
        });
        ps.println("");
        ps.println("settings wrapped by " + decorators.toString());
    }
    private String id;
    private final Map help = new HashMap<>(50);
    private final List> decorators
            = new ArrayList<>(3);
    private AbstractPropertiesDecorator outermostDecorator;

    /**
     * Creates a backing map {@link HashMap#HashMap() ) }.
     *
     */
    public Settings() {
        backingMap = new HashMap<>();
    }

    /**
     * Creates a backing map {@link HashMap#HashMap(int, float) }.
     *
     * @param initialCapacity
     * @param loadFactor
     */
    public Settings(int initialCapacity, float loadFactor) {
        backingMap = new HashMap<>(initialCapacity, loadFactor);
    }

    /**
     * Creates a backing map {@link HashMap#HashMap(int) }.
     *
     * @param initialCapacity
     */
    public Settings(int initialCapacity) {
        backingMap = new HashMap<>(initialCapacity);
    }

    /**
     * Uses the provided Map as backing map, throws an IllegalArgumentException
     * if the map is an instance of EnhancedMap.
     *
     * @param map
     */
    public Settings(Map map) {
        Objects.requireNonNull(map);
        if (map instanceof EnhancedMap) {
            throw new IllegalArgumentException("instance of " + EnhancedMap.class.getName() + " not allowed");
        }
        backingMap = new HashMap<>(map);
    }

    private void debug(Object val, String... keys) {
        debug(val, true, keys);
    }

    private void debug(Object val, boolean defaultVal, String... keys) {
        if (log.isLoggable(Level.FINE)) {
            StringBuilder s = new StringBuilder(String.valueOf(val));
            if (val != null && val.getClass().isArray()) {
                s = new StringBuilder("");
                Class compType = val.getClass().getComponentType();
                if (!compType.isPrimitive()) {
                    for (Object o : (Object[]) val) {
                        s.append(String.valueOf(o)).append("; ");
                    }
                } else if (compType.equals(boolean.class)) {
                    for (boolean o : (boolean[]) val) {
                        s.append(String.valueOf(o)).append("; ");
                    }
                } else if (compType.equals(char.class)) {
                    for (char o : (char[]) val) {
                        s.append(String.valueOf(o)).append("; ");
                    }
                } else if (compType.equals(byte.class)) {
                    for (byte o : (byte[]) val) {
                        s.append(String.valueOf(o)).append("; ");
                    }
                } else if (compType.equals(short.class)) {
                    for (short o : (short[]) val) {
                        s.append(String.valueOf(o)).append("; ");
                    }
                } else if (compType.equals(int.class)) {
                    for (int o : (int[]) val) {
                        s.append(String.valueOf(o)).append("; ");
                    }
                } else if (compType.equals(long.class)) {
                    for (long o : (long[]) val) {
                        s.append(String.valueOf(o)).append("; ");
                    }
                } else if (compType.equals(float.class)) {
                    for (float o : (float[]) val) {
                        s.append(String.valueOf(o)).append("; ");
                    }
                } else if (compType.equals(double.class)) {
                    for (double o : (double[]) val) {
                        s.append(String.valueOf(o)).append("; ");
                    }
                }
            }
            log.fine(String.format("looking for property %s in %s, using value %s", Arrays.asList(keys), getId(), s.append((defaultVal) ? " (default)" : "").toString()));
        }
    }

    @Override
    public final String[] get(Object key) {
        unused.remove(key);
        return backingMap.get(key);
    }

    private String getFirst(String key) {
        if (log.isLoggable(Level.FINE)) {
            debug((key != null) ? backingMap.get(key) : null, false, key);
        }
        if (key == null) {
            return null;
        }
        String[] l = get(key);
        if (l != null) {
            if (l.length > 1) {
                throw new VectorPrintRuntimeException(String.format("more then one value (%s) for %s, expected one",
                        getFactory().getBindingHelper().serializeValue(l), key));
            }
            return l.length == 0 ? null : l[0];
        } else {
            return null;
        }

    }

    /**
     * When only one argument is given it is assumed to be a key and not a
     * default value.
     *
     * @param defaultValue
     * @param keys
     * @return
     */
    @Override
    public String getProperty(String defaultValue, String... keys) {
        boolean defaultIsKey = defaultValue != null && (keys == null || keys.length == 0);
        String key = defaultIsKey
                ? getKey(null, defaultValue)
                : getKey(defaultValue, keys);
        return key == null ? defaultValue : getFirst(key);
    }

    @Override
    public URL getURLProperty(URL defaultValue, String... keys) throws MalformedURLException {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), URL.class);
    }

    @Override
    public File getFileProperty(File defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), File.class);
    }

    @Override
    public float getFloatProperty(Float defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Float.class);
    }

    @Override
    public boolean getBooleanProperty(Boolean defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Boolean.class);
    }

    /**
     * determine the {@link #determineKey(java.lang.String...) key to be used}, call {@link #handleNoValue(java.lang.String...) } when no key is found
     * and no default is given.
     *
     * @param defaultVal a default value
     * @param keys the keys to look for
     * @return the key to be used or null to use a default value
     */
    private String getKey(Object defaultVal, String... keys) {
        String key = determineKey(keys);
        if (key == null) {
            if (defaultVal == null) {
                handleNoValue(keys);
            } else {
                debug(defaultVal, key);
            }
        } else {
            return key;
        }
        return null;
    }

    /**
     * throws a {@link NoValueException}
     * @param keys
     */
    private void handleNoValue(String... keys) {
        throw new NoValueException(Arrays.asList(keys) + " not found and default is null");
    }

    /**
     * Return the first key from the list of arguments present in the settings.
     * Logs the key that is returned, maintains
     * {@link #getKeysNotPresent() set of keys not present}.
     * 
     * @see #keyNotPresent(java.lang.String) 
     * @see #keyPresent(java.lang.String) 
     *
     * @param keys
     * @return the first key found in settings or null
     */
    private String determineKey(String... keys) {
        if (keys == null || keys.length == 0 || keys[0] == null) {
            throw new VectorPrintRuntimeException("You should provide at least one key");
        }
        if (keys.length == 1) {
            if (containsKey(keys[0])) {
                if (log.isLoggable(Level.FINE)) {
                    log.fine(String.format("Returning key \"%s\" from %s, it was first found in settings", keys[0], Arrays.asList(keys)));
                }
                if (notPresent.contains(keys[0])) {
                    notPresent.remove(keys[0]);
                }
                return keys[0];
            } else {
                if (!notPresent.contains(keys[0])) {
                    notPresent.add(keys[0]);
                }
                return null;
            }
        }
        for (String k : keys) {
            if (containsKey(k)) {
                if (log.isLoggable(Level.FINE)) {
                    log.fine(String.format("Returning key \"%s\" from %s, it was first found in settings", k, Arrays.asList(keys)));
                }
                if (notPresent.contains(keys[0])) {
                    notPresent.remove(keys[0]);
                }
                return k;
            } else if (!notPresent.contains(k)) {
                notPresent.add(k);
            }

        }
        if (log.isLoggable(Level.FINE)) {
            log.fine(String.format("None of %s found in settings", Arrays.asList(keys)));
        }
        return null;
    }

    @Override
    public double getDoubleProperty(Double defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Double.class);
    }

    @Override
    public int getIntegerProperty(Integer defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Integer.class);
    }

    @Override
    public short getShortProperty(Short defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Short.class);
    }

    @Override
    public char getCharProperty(Character defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Character.class);
    }

    @Override
    public byte getByteProperty(Byte defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Byte.class);
    }

    @Override
    public long getLongProperty(Long defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Long.class);
    }

    @Override
    public Color getColorProperty(Color defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Color.class);
    }

    @Override
    public String[] getStringProperties(String[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        if (key == null) {
            return defaultValue;
        }
        return get(key);
    }

    @Override
    public URL[] getURLProperties(URL[] defaultValue, String... keys) throws MalformedURLException {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseURLValues(get(key));
    }

    @Override
    public File[] getFileProperties(File[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseFileValues(get(key));
    }

    @Override
    public float[] getFloatProperties(float[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseFloatValues(get(key));
    }

    @Override
    public char[] getCharProperties(char[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseCharValues(get(key));

    }

    @Override
    public short[] getShortProperties(short[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseShortValues(get(key));
    }

    @Override
    public byte[] getByteProperties(byte[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseByteValues(get(key));
    }

    @Override
    public double[] getDoubleProperties(double[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseDoubleValues(get(key));
    }

    @Override
    public int[] getIntegerProperties(int[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseIntValues(get(key));
    }

    @Override
    public boolean[] getBooleanProperties(boolean[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseBooleanValues(get(key));
    }

    @Override
    public Color[] getColorProperties(Color[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseColorValues(get(key));
    }

    /**
     * This implementation only includes key and value state, not the rest of
     * the state (i.e. {@link #getUnusedKeys() }, {@link #getHelp()
     * } and {@link #getKeysNotPresent() }). The {@link #getId() id's} of the
     * objects must be both null or the same, otherwise false is returned.
     *
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Settings other = (Settings) obj;
        if ((this.id == null) ? (other.id != null) : !this.id.equals(other.id)) {
            return false;
        }
        return Objects.equals(backingMap, other.backingMap);
    }

    @Override
    public PropertyHelp getHelp(String key) {
        if (help.containsKey(key)) {
            return help.get(key);
        } else {
            return new PropertyHelpImpl("no help configured for " + key);
        }
    }

    /**
     * initializes help for properties
     *
     * @param help
     * @see #getHelp(java.lang.String)
     * @see #getHelp()
     */
    @Override
    public void setHelp(Map help) {
        this.help.clear();
        this.help.putAll(help);
    }

    @Override
    public Map getHelp() {
        return help;
    }

    @Override
    public String printHelp() {
        StringBuilder sb = new StringBuilder(1024);
        help.entrySet().forEach((h) -> {
            sb.append(h.getKey()).append(": ").append(h.getValue().getType())
                    .append("; ")
                    .append(h.getValue().getExplanation())
                    .append(System.getProperty("line.separator"));
        });
        return sb.toString();
    }

    @Override
    public long[] getLongProperties(long[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseLongValues(get(key));
    }

    @Override
    public final String[] put(String key, String[] value) {
        if (!backingMap.containsKey(key)) unused.add(key);
        return backingMap.put(key, value);
    }

    @Override
    public final void clear() {
        unused.clear();
        notPresent.clear();
        help.clear();
        decorators.clear();
        backingMap.clear();
    }

    @Override
    public final String[] remove(Object key) {
        unused.remove(key);
        return backingMap.remove(key);
    }

    private void init(Settings vp) {
        vp.help.putAll(help);
        vp.unused.addAll(keySet());
        vp.id = id;
        vp.notPresent.addAll(notPresent);
        vp.decorators.addAll(decorators);
        vp.outermostDecorator = outermostDecorator;
    }

    /**
     * Creates a new identical Settings object. Note that the id of the clone
     * will equal that of the original. The backing Map will be cloned by
     * calling clone when it is Cloneable, otherwise newInstance and putAll are
     * used to clone the backing Map. The
     * {@link #getOutermostDecorator() outermostDecorator} will be copied, not
     * cloned.
     *
     * @return an identical copy of these Settings.
     */
    @Override
    public Settings clone() {
        Settings vp;
        if (backingMap instanceof Cloneable) {
            try {
                Method m = backingMap.getClass().getMethod("clone");
                vp = new Settings((Map) m.invoke(backingMap, null));
            } catch (InvocationTargetException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException | SecurityException ex) {
                throw new VectorPrintRuntimeException(ex);
            }
        } else {
            try {
                vp = new Settings(backingMap.getClass().newInstance());
            } catch (IllegalAccessException | InstantiationException ex) {
                throw new VectorPrintRuntimeException(ex);
            }
            vp.backingMap.putAll(backingMap);
        }
        init(vp);
        return vp;
    }

    /**
     *
     * @return the id of the properties or null
     */
    @Override
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    /**
     * this implementation supports all primitives and their wrappers, Color,
     * Date, URL, Class and arrays of those types. Calls {@link #getGenericProperty(java.lang.String, java.lang.Object, java.lang.Class)
     * } if one of the keys is present in the settings.
     *
     * @param 
     * @param keys
     * @param defaultValue
     * @param clazz
     * @return value of the setting or the default value
     * @throws VectorPrintRuntimeException when no value is found and
     * defaultValue is null
     */
    public  T getGenericProperty(T defaultValue, Class clazz, String... keys) {
        String key = getKey(defaultValue, keys);
        return getGenericProperty(key, defaultValue, clazz);
    }

    /**
     * this implementation supports all primitives and their wrappers, Color,
     * Date, URL, Class and arrays of those types.
     *
     * @param 
     * @param key
     * @param defaultValue
     * @param clazz
     * @return value of the setting or the default value
     * defaultValue is null
     */
    private  T getGenericProperty(String key, T defaultValue, Class clazz) {
        if (key == null) {
            return defaultValue;
        } else if (clazz.isArray()) {
            if (String[].class.equals(clazz)) {
                return (T) get(key);
            }
            return key == null ? defaultValue : getFactory().getBindingHelper().convert(get(key), clazz);
        } else {
            if (String.class.equals(clazz)) {
                return (T) getFirst(key);
            }
            return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), clazz);
        }
    }

    @Override
    public Date getDateProperty(Date defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Date.class);
    }

    @Override
    public Date[] getDateProperties(Date[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseDateValues(get(key));
    }

    private final Collection unused = new HashSet<>(25);

    @Override
    public Collection getUnusedKeys() {
        for (Iterator it = unused.iterator(); it.hasNext();) {
            if (!containsKey(it.next())) {
                it.remove();
            }
        }
        return Collections.unmodifiableCollection(unused);
    }

    private final Set notPresent = new HashSet<>(25);

    @Override
    public Collection getKeysNotPresent() {
        return Collections.unmodifiableCollection(notPresent);
    }

    @Override
    protected void finalize() throws Throwable {
        log.warning("not used, possibly obsolete settings in: " + this + ": " + getUnusedKeys());
        super.finalize();
    }

    @Override
    public String toString() {
        return "Settings{" + "id=" + id + ", decorators=" + decorators + '}';
    }
    
    

    /**
     *
     * @param defaultValue the value of defaultValue
     * @param keys the value of keys
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class getClassProperty(Class defaultValue, String... keys) throws ClassNotFoundException {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Class.class);
    }

    @Override
    public Pattern getRegexProperty(Pattern defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : getFactory().getBindingHelper().convert(getFirst(key), Pattern.class);
    }

    @Override
    public Pattern[] getRegexProperties(Pattern[] defaultValue, String... keys) {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseRegexValues(get(key));
    }

    /**
     *
     * @param defaultValue the value of defaultValue
     * @param keys the value of keys
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class[] getClassProperties(Class[] defaultValue, String... keys) throws ClassNotFoundException {
        String key = getKey(defaultValue, keys);
        return key == null ? defaultValue : AbstractBindingHelperDecorator.parseClassValues(get(key));
    }

    @Override
    public List> getDecorators() {
        return decorators;
    }

    @Override
    public void addDecorator(Class clazz) {
        decorators.add(clazz);
    }

    @Override
    public AbstractPropertiesDecorator getOutermostDecorator() {
        return outermostDecorator;
    }

    @Override
    public void setOutermostDecorator(AbstractPropertiesDecorator outermostDecorator) {
        this.outermostDecorator = outermostDecorator;
        log.warning(String.format("NB! Settings wrapped by %s, you should use this instead of %s", outermostDecorator.getClass().getName(),
                getClass().getName()));
    }

    @Override
    public String[] put(String key, String value) {
        return put(key, new String[]{value});
    }

    @Override
    public boolean containsValue(Object value) {
        if (value == null) {
            return backingMap.containsValue(null);
        }
        if (String[].class.equals(value.getClass())) {

            if (entrySet().stream().anyMatch((e) -> (Arrays.equals(e.getValue(), (String[]) value)))) {
                return true;
            }
        }
        return false;
    }

    private EnhancedMapBindingFactory getFactory() {
        return SettingsBindingService.getInstance().getFactory();
    }

    @Override
    public int size() {
        return backingMap.size();
    }

    @Override
    public boolean isEmpty() {
        return backingMap.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return backingMap.containsKey(key);
    }

    @Override
    public void putAll(Map m) {
        m.forEach((k, v) -> {
            if (!backingMap.containsKey(k)) unused.add(k);
        });
        backingMap.putAll(m);
    }

    @Override
    public void put(Map m) {
        m.forEach((k, v) -> {
            put(k, v);
        });
    }

    @Override
    public Set keySet() {
        return backingMap.keySet();
    }

    @Override
    public Collection values() {
        return backingMap.values();
    }

    @Override
    public Set> entrySet() {
        return backingMap.entrySet();
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy