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

org.apache.juneau.uon.UonParserSession Maven / Gradle / Ivy

There is a newer version: 9.0.1
Show newest version
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you 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.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.uon;

import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.uon.UonParser.*;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;

import org.apache.juneau.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.transform.*;

/**
 * Session object that lives for the duration of a single use of {@link UonParser}.
 *
 * 

* This class is NOT thread safe. * It is typically discarded after one-time use although it can be reused against multiple inputs. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public class UonParserSession extends ReaderParserSession implements HttpPartParserSession { // Characters that need to be preceded with an escape character. private static final AsciiSet escapedChars = AsciiSet.create("~'\u0001\u0002"); private static final char AMP='\u0001', EQ='\u0002'; // Flags set in reader to denote & and = characters. private final UonParser ctx; private final boolean decodeChars; /** * Create a new session using properties specified in the context. * * @param ctx * The context creating this session object. * The context contains all the configuration settings for this object. * @param args * Runtime session arguments. */ protected UonParserSession(UonParser ctx, ParserSessionArgs args) { super(ctx, args); this.ctx = ctx; decodeChars = getProperty(UON_decoding, boolean.class, ctx.isDecodeChars()); } @Override /* Session */ public ObjectMap asMap() { return super.asMap() .append("UonParser", new ObjectMap() .append("decodeChars", decodeChars) ); } /** * Create a specialized parser session for parsing URL parameters. * *

* The main difference is that characters are never decoded, and the {@link UonParser#UON_decoding} * property is always ignored. * * @param ctx * The context creating this session object. * The context contains all the configuration settings for this object. * @param args * Runtime session arguments. * @param decodeChars * Whether to decode characters. */ protected UonParserSession(UonParser ctx, ParserSessionArgs args, boolean decodeChars) { super(ctx, args); this.ctx = ctx; this.decodeChars = decodeChars; } @Override /* ParserSession */ protected T doParse(ParserPipe pipe, ClassMeta type) throws Exception { try (UonReader r = getUonReader(pipe, decodeChars)) { T o = parseAnything(type, r, getOuter(), true, null); validateEnd(r); return o; } } @Override /* ReaderParserSession */ protected Map doParseIntoMap(ParserPipe pipe, Map m, Type keyType, Type valueType) throws Exception { try (UonReader r = getUonReader(pipe, decodeChars)) { m = parseIntoMap(r, m, (ClassMeta)getClassMeta(keyType), (ClassMeta)getClassMeta(valueType), null); validateEnd(r); return m; } } @Override /* ReaderParserSession */ protected Collection doParseIntoCollection(ParserPipe pipe, Collection c, Type elementType) throws Exception { try (UonReader r = getUonReader(pipe, decodeChars)) { c = parseIntoCollection(r, c, (ClassMeta)getClassMeta(elementType), false, null); validateEnd(r); return c; } } @Override /* HttpPartParser */ public T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta toType) throws ParseException, SchemaValidationException { if (in == null) return null; if (toType.isString() && in.length() > 0) { // Shortcut - If we're returning a string and the value doesn't start with "'" or is "null", then // just return the string since it's a plain value. // This allows us to bypass the creation of a UonParserSession object. char x = firstNonWhitespaceChar(in); if (x != '\'' && x != 'n' && in.indexOf('~') == -1) return (T)in; if (x == 'n' && "null".equals(in)) return null; } try (ParserPipe pipe = createPipe(in)) { try (UonReader r = getUonReader(pipe, false)) { return parseAnything(toType, r, null, true, null); } } catch (ParseException e) { throw e; } catch (Exception e) { throw new ParseException(e); } } @Override /* HttpPartParserSession */ public T parse(HttpPartType partType, HttpPartSchema schema, String in, Class toType) throws ParseException, SchemaValidationException { return parse(null, schema, in, getClassMeta(toType)); } @Override /* HttpPartParserSession */ public T parse(HttpPartType partType, HttpPartSchema schema, String in, Type toType, Type...toTypeArgs) throws ParseException, SchemaValidationException { return (T)parse(null, schema, in, getClassMeta(toType, toTypeArgs)); } @Override /* HttpPartParserSession */ public T parse(HttpPartSchema schema, String in, Class toType) throws ParseException, SchemaValidationException { return parse(null, schema, in, getClassMeta(toType)); } @Override /* HttpPartParserSession */ public T parse(HttpPartSchema schema, String in, ClassMeta toType) throws ParseException, SchemaValidationException { return parse(null, schema, in, toType); } @Override /* HttpPartParserSession */ public T parse(HttpPartSchema schema, String in, Type toType, Type...toTypeArgs) throws ParseException, SchemaValidationException { return (T)parse(null, schema, in, getClassMeta(toType, toTypeArgs)); } /** * Workhorse method. * * @param eType The class type being parsed, or null if unknown. * @param r The reader being parsed. * @param outer The outer object (for constructing nested inner classes). * @param isUrlParamValue * If true, then we're parsing a top-level URL-encoded value which is treated a bit different than the * default case. * @param pMeta The current bean property being parsed. * @return The parsed object. * @throws Exception */ public T parseAnything(ClassMeta eType, UonReader r, Object outer, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws Exception { if (eType == null) eType = object(); PojoSwap swap = (PojoSwap)eType.getPojoSwap(this); BuilderSwap builder = (BuilderSwap)eType.getBuilderSwap(this); ClassMeta sType = null; if (builder != null) sType = builder.getBuilderClassMeta(this); else if (swap != null) sType = swap.getSwapClassMeta(this); else sType = eType; setCurrentClass(sType); Object o = null; int c = r.peekSkipWs(); if (c == -1 || c == AMP) { // If parameter is blank and it's an array or collection, return an empty list. if (sType.isCollectionOrArray()) o = sType.newInstance(); else if (sType.isString() || sType.isObject()) o = ""; else if (sType.isPrimitive()) o = sType.getPrimitiveDefault(); // Otherwise, leave null. } else if (sType.isVoid()) { String s = parseString(r, isUrlParamValue); if (s != null) throw new ParseException(this, "Expected ''null'' for void value, but was ''{0}''.", s); } else if (sType.isObject()) { if (c == '(') { ObjectMap m = new ObjectMap(this); parseIntoMap(r, m, string(), object(), pMeta); o = cast(m, pMeta, eType); } else if (c == '@') { Collection l = new ObjectList(this); o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta); } else { String s = parseString(r, isUrlParamValue); if (c != '\'') { if ("true".equals(s) || "false".equals(s)) o = Boolean.valueOf(s); else if (! "null".equals(s)) { if (isNumeric(s)) o = StringUtils.parseNumber(s, Number.class); else o = s; } } else { o = s; } } } else if (sType.isBoolean()) { o = parseBoolean(r); } else if (sType.isCharSequence()) { o = parseString(r, isUrlParamValue); } else if (sType.isChar()) { o = parseCharacter(parseString(r, isUrlParamValue)); } else if (sType.isNumber()) { o = parseNumber(r, (Class)sType.getInnerClass()); } else if (sType.isMap()) { Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new ObjectMap(this)); o = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta); } else if (sType.isCollection()) { if (c == '(') { ObjectMap m = new ObjectMap(this); parseIntoMap(r, m, string(), object(), pMeta); // Handle case where it's a collection, but serialized as a map with a _type or _value key. if (m.containsKey(getBeanTypePropertyName(sType))) o = cast(m, pMeta, eType); // Handle case where it's a collection, but only a single value was specified. else { Collection l = ( sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : new ObjectList(this) ); l.add(m.cast(sType.getElementType())); o = l; } } else { Collection l = ( sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : new ObjectList(this) ); o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta); } } else if (builder != null) { BeanMap m = toBeanMap(builder.create(this, eType)); m = parseIntoBeanMap(r, m); o = m == null ? null : builder.build(this, m.getBean(), eType); } else if (sType.canCreateNewBean(outer)) { BeanMap m = newBeanMap(outer, sType.getInnerClass()); m = parseIntoBeanMap(r, m); o = m == null ? null : m.getBean(); } else if (sType.canCreateNewInstanceFromString(outer)) { String s = parseString(r, isUrlParamValue); if (s != null) o = sType.newInstanceFromString(outer, s); } else if (sType.canCreateNewInstanceFromNumber(outer)) { o = sType.newInstanceFromNumber(this, outer, parseNumber(r, sType.getNewInstanceFromNumberClass())); } else if (sType.isArray() || sType.isArgs()) { if (c == '(') { ObjectMap m = new ObjectMap(this); parseIntoMap(r, m, string(), object(), pMeta); // Handle case where it's an array, but serialized as a map with a _type or _value key. if (m.containsKey(getBeanTypePropertyName(sType))) o = cast(m, pMeta, eType); // Handle case where it's an array, but only a single value was specified. else { ArrayList l = new ArrayList(1); l.add(m.cast(sType.getElementType())); o = toArray(sType, l); } } else { ArrayList l = (ArrayList)parseIntoCollection(r, new ArrayList(), sType, isUrlParamValue, pMeta); o = toArray(sType, l); } } else if (c == '(') { // It could be a non-bean with _type attribute. ObjectMap m = new ObjectMap(this); parseIntoMap(r, m, string(), object(), pMeta); if (m.containsKey(getBeanTypePropertyName(sType))) o = cast(m, pMeta, eType); else throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", sType.getInnerClass().getName(), sType.getNotABeanReason()); } else if (c == 'n') { r.read(); parseNull(r); } else { throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", sType.getInnerClass().getName(), sType.getNotABeanReason()); } if (o == null && sType.isPrimitive()) o = sType.getPrimitiveDefault(); if (swap != null && o != null) o = swap.unswap(this, o, eType); if (outer != null) setParent(eType, o, outer); return (T)o; } private Map parseIntoMap(UonReader r, Map m, ClassMeta keyType, ClassMeta valueType, BeanPropertyMeta pMeta) throws Exception { if (keyType == null) keyType = (ClassMeta)string(); int c = r.read(); if (c == -1 || c == AMP) return null; if (c == 'n') return (Map)parseNull(r); if (c != '(') throw new ParseException(this, "Expected '(' at beginning of object."); final int S1=1; // Looking for attrName start. final int S2=2; // Found attrName end, looking for =. final int S3=3; // Found =, looking for valStart. final int S4=4; // Looking for , or ) boolean isInEscape = false; int state = S1; K currAttr = null; while (c != -1 && c != AMP) { c = r.read(); if (! isInEscape) { if (state == S1) { if (c == ')') return m; if (Character.isWhitespace(c)) skipSpace(r); else { r.unread(); Object attr = parseAttr(r, decodeChars); currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType); state = S2; c = 0; // Avoid isInEscape if c was '\' } } else if (state == S2) { if (c == EQ || c == '=') state = S3; else if (c == -1 || c == ',' || c == ')' || c == AMP) { if (currAttr == null) { // Value was '%00' r.unread(); return null; } m.put(currAttr, null); if (c == ')' || c == -1 || c == AMP) return m; state = S1; } } else if (state == S3) { if (c == -1 || c == ',' || c == ')' || c == AMP) { V value = convertAttrToType(m, "", valueType); m.put(currAttr, value); if (c == -1 || c == ')' || c == AMP) return m; state = S1; } else { V value = parseAnything(valueType, r.unread(), m, false, pMeta); setName(valueType, value, currAttr); m.put(currAttr, value); state = S4; c = 0; // Avoid isInEscape if c was '\' } } else if (state == S4) { if (c == ',') state = S1; else if (c == ')' || c == -1 || c == AMP) { return m; } } } isInEscape = isInEscape(c, r, isInEscape); } if (state == S1) throw new ParseException(this, "Could not find attribute name on object."); if (state == S2) throw new ParseException(this, "Could not find '=' following attribute name on object."); if (state == S3) throw new ParseException(this, "Dangling '=' found in object entry"); if (state == S4) throw new ParseException(this, "Could not find ')' marking end of object."); return null; // Unreachable. } private Collection parseIntoCollection(UonReader r, Collection l, ClassMeta type, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws Exception { int c = r.readSkipWs(); if (c == -1 || c == AMP) return null; if (c == 'n') return (Collection)parseNull(r); int argIndex = 0; // If we're parsing a top-level parameter, we're allowed to have comma-delimited lists outside parenthesis (e.g. "&foo=1,2,3&bar=a,b,c") // This is not allowed at lower levels since we use comma's as end delimiters. boolean isInParens = (c == '@'); if (! isInParens) { if (isUrlParamValue) r.unread(); else throw new ParseException(this, "Could not find '(' marking beginning of collection."); } else { r.read(); } if (isInParens) { final int S1=1; // Looking for starting of first entry. final int S2=2; // Looking for starting of subsequent entries. final int S3=3; // Looking for , or ) after first entry. int state = S1; while (c != -1 && c != AMP) { c = r.read(); if (state == S1 || state == S2) { if (c == ')') { if (state == S2) { l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, false, pMeta)); r.read(); } return l; } else if (Character.isWhitespace(c)) { skipSpace(r); } else { l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, false, pMeta)); state = S3; } } else if (state == S3) { if (c == ',') { state = S2; } else if (c == ')') { return l; } } } if (state == S1 || state == S2) throw new ParseException(this, "Could not find start of entry in array."); if (state == S3) throw new ParseException(this, "Could not find end of entry in array."); } else { final int S1=1; // Looking for starting of entry. final int S2=2; // Looking for , or & or END after first entry. int state = S1; while (c != -1 && c != AMP) { c = r.read(); if (state == S1) { if (Character.isWhitespace(c)) { skipSpace(r); } else { l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, false, pMeta)); state = S2; } } else if (state == S2) { if (c == ',') { state = S1; } else if (Character.isWhitespace(c)) { skipSpace(r); } else if (c == AMP || c == -1) { r.unread(); return l; } } } } return null; // Unreachable. } private BeanMap parseIntoBeanMap(UonReader r, BeanMap m) throws Exception { int c = r.readSkipWs(); if (c == -1 || c == AMP) return null; if (c == 'n') return (BeanMap)parseNull(r); if (c != '(') throw new ParseException(this, "Expected '(' at beginning of object."); final int S1=1; // Looking for attrName start. final int S2=2; // Found attrName end, looking for =. final int S3=3; // Found =, looking for valStart. final int S4=4; // Looking for , or } boolean isInEscape = false; int state = S1; String currAttr = ""; mark(); try { while (c != -1 && c != AMP) { c = r.read(); if (! isInEscape) { if (state == S1) { if (c == ')' || c == -1 || c == AMP) { return m; } if (Character.isWhitespace(c)) skipSpace(r); else { r.unread(); mark(); currAttr = parseAttrName(r, decodeChars); if (currAttr == null) { // Value was '%00' return null; } state = S2; } } else if (state == S2) { if (c == EQ || c == '=') state = S3; else if (c == -1 || c == ',' || c == ')' || c == AMP) { m.put(currAttr, null); if (c == ')' || c == -1 || c == AMP) { return m; } state = S1; } } else if (state == S3) { if (c == -1 || c == ',' || c == ')' || c == AMP) { if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); if (pMeta == null) { onUnknownProperty(currAttr, m); unmark(); } else { unmark(); Object value = convertToType("", pMeta.getClassMeta()); pMeta.set(m, currAttr, value); } } if (c == -1 || c == ')' || c == AMP) return m; state = S1; } else { if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); if (pMeta == null) { onUnknownProperty(currAttr, m); unmark(); parseAnything(object(), r.unread(), m.getBean(false), false, null); // Read content anyway to ignore it } else { unmark(); setCurrentProperty(pMeta); ClassMeta cm = pMeta.getClassMeta(); Object value = parseAnything(cm, r.unread(), m.getBean(false), false, pMeta); setName(cm, value, currAttr); pMeta.set(m, currAttr, value); setCurrentProperty(null); } } state = S4; } } else if (state == S4) { if (c == ',') state = S1; else if (c == ')' || c == -1 || c == AMP) { return m; } } } isInEscape = isInEscape(c, r, isInEscape); } if (state == S1) throw new ParseException(this, "Could not find attribute name on object."); if (state == S2) throw new ParseException(this, "Could not find '=' following attribute name on object."); if (state == S3) throw new ParseException(this, "Could not find value following '=' on object."); if (state == S4) throw new ParseException(this, "Could not find ')' marking end of object."); } finally { unmark(); } return null; // Unreachable. } private Object parseNull(UonReader r) throws Exception { String s = parseString(r, false); if ("ull".equals(s)) return null; throw new ParseException(this, "Unexpected character sequence: ''{0}''", s); } /** * Convenience method for parsing an attribute from the specified parser. * * @param r * @param encoded * @return The parsed object * @throws Exception */ protected final Object parseAttr(UonReader r, boolean encoded) throws Exception { Object attr; attr = parseAttrName(r, encoded); return attr; } /** * Parses an attribute name from the specified reader. * * @param r * @param encoded * @return The parsed attribute name. * @throws Exception */ protected final String parseAttrName(UonReader r, boolean encoded) throws Exception { // If string is of form 'xxx', we're looking for ' at the end. // Otherwise, we're looking for '&' or '=' or WS or -1 denoting the end of this string. int c = r.peekSkipWs(); if (c == '\'') return parsePString(r); r.mark(); boolean isInEscape = false; if (encoded) { while (c != -1) { c = r.read(); if (! isInEscape) { if (c == AMP || c == EQ || c == -1 || Character.isWhitespace(c)) { if (c != -1) r.unread(); String s = r.getMarked(); return ("null".equals(s) ? null : s); } } else if (c == AMP) r.replace('&'); else if (c == EQ) r.replace('='); isInEscape = isInEscape(c, r, isInEscape); } } else { while (c != -1) { c = r.read(); if (! isInEscape) { if (c == '=' || c == -1 || Character.isWhitespace(c)) { if (c != -1) r.unread(); String s = r.getMarked(); return ("null".equals(s) ? null : trim(s)); } } isInEscape = isInEscape(c, r, isInEscape); } } // We should never get here. throw new ParseException(this, "Unexpected condition."); } /* * Returns true if the next character in the stream is preceded by an escape '~' character. */ private static final boolean isInEscape(int c, ParserReader r, boolean prevIsInEscape) throws Exception { if (c == '~' && ! prevIsInEscape) { c = r.peek(); if (escapedChars.contains(c)) { r.delete(); return true; } } return false; } /** * Parses a string value from the specified reader. * * @param r * @param isUrlParamValue * @return The parsed string. * @throws Exception */ protected final String parseString(UonReader r, boolean isUrlParamValue) throws Exception { // If string is of form 'xxx', we're looking for ' at the end. // Otherwise, we're looking for ',' or ')' or -1 denoting the end of this string. int c = r.peekSkipWs(); if (c == '\'') return parsePString(r); r.mark(); boolean isInEscape = false; String s = null; AsciiSet endChars = (isUrlParamValue ? endCharsParam : endCharsNormal); while (c != -1) { c = r.read(); if (! isInEscape) { // If this is a URL parameter value, we're looking for: & // If not, we're looking for: &,) if (endChars.contains(c)) { r.unread(); c = -1; } } if (c == -1) s = r.getMarked(); else if (c == EQ) r.replace('='); else if (Character.isWhitespace(c) && ! isUrlParamValue) { s = r.getMarked(0, -1); skipSpace(r); c = -1; } isInEscape = isInEscape(c, r, isInEscape); } if (isUrlParamValue) s = StringUtils.trim(s); return ("null".equals(s) ? null : trim(s)); } private static final AsciiSet endCharsParam = AsciiSet.create(""+AMP), endCharsNormal = AsciiSet.create(",)"+AMP); /* * Parses a string of the form "'foo'" * All whitespace within parenthesis are preserved. */ private String parsePString(UonReader r) throws Exception { r.read(); // Skip first quote. r.mark(); int c = 0; boolean isInEscape = false; while (c != -1) { c = r.read(); if (! isInEscape) { if (c == '\'') return trim(r.getMarked(0, -1)); } if (c == EQ) r.replace('='); isInEscape = isInEscape(c, r, isInEscape); } throw new ParseException(this, "Unmatched parenthesis"); } private Boolean parseBoolean(UonReader r) throws Exception { String s = parseString(r, false); if (s == null || s.equals("null")) return null; if (s.equalsIgnoreCase("true")) return true; if (s.equalsIgnoreCase("false")) return false; throw new ParseException(this, "Unrecognized syntax for boolean. ''{0}''.", s); } private Number parseNumber(UonReader r, Class c) throws Exception { String s = parseString(r, false); if (s == null) return null; return StringUtils.parseNumber(s, c); } /* * Call this method after you've finished a parsing a string to make sure that if there's any * remainder in the input, that it consists only of whitespace and comments. */ private void validateEnd(UonReader r) throws Exception { if (! isValidateEnd()) return; while (true) { int c = r.read(); if (c == -1) return; if (! Character.isWhitespace(c)) throw new ParseException(this, "Remainder after parse: ''{0}''.", (char)c); } } private static void skipSpace(ParserReader r) throws Exception { int c = 0; while ((c = r.read()) != -1) { if (c <= 2 || ! Character.isWhitespace(c)) { r.unread(); return; } } } /** * Creates a {@link UonReader} from the specified parser pipe. * * @param pipe The parser input. * @param decodeChars Whether the reader should automatically decode URL-encoded characters. * @return A new {@link UonReader} object. * @throws Exception */ public final UonReader getUonReader(ParserPipe pipe, boolean decodeChars) throws Exception { Reader r = pipe.getReader(); if (r instanceof UonReader) return (UonReader)r; return new UonReader(pipe, decodeChars); } //----------------------------------------------------------------------------------------------------------------- // Properties //----------------------------------------------------------------------------------------------------------------- /** * Configuration property: Decode "%xx" sequences. * * @see UonParser#UON_decoding * @return * true if URI encoded characters should be decoded, false if they've already been decoded * before being passed to this parser. */ protected final boolean isDecodeChars() { return decodeChars; } /** * Configuration property: Validate end. * * @see UonParser#UON_validateEnd * @return * true if after parsing a POJO from the input, verifies that the remaining input in * the stream consists of only comments or whitespace. */ protected final boolean isValidateEnd() { return ctx.isValidateEnd(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy