Java tutorial
/* * 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 com.aliyun.odps.conf; import java.io.DataInput; import java.io.DataOutput; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.xml.sax.SAXException; import com.aliyun.odps.Survey; import com.aliyun.odps.io.Writable; import com.aliyun.odps.io.WritableUtils; /** * ??????. * * <h4 id="Resources">?</h4> * <p> * ???????? XML? name/value * * <pre> * <configuration> * <property> * <name>com.mycomp.xxx</name> * <value>xxx</value> * </property> * ... ... * </configuration> * </pre> * * {@link #writeXml(OutputStream)} ??? * </p> * * <h4 id="VariableExpansion">??</h4> * <p> * ????${user.name}??? * <ol> * <li>????? {@link System#getProperties()} ??? * <li>?????? ????</li> * </ol> * * <p> * ?? * * <pre> * <property> * <name>basedir</name> * <value>/user/${<i>user.name</i>}</value> * </property> * * <property> * <name>tempdir</name> * <value>${<i>basedir</i>}/tmp</value> * </property> * </pre> */ public class Configuration implements Iterable<Map.Entry<String, String>>, Writable { static class StringUtils { /** * Returns a collection of strings. * * @param str * comma seperated string values * @return an <code>ArrayList</code> of string values */ static Collection<String> getStringCollection(String str) { List<String> values = new ArrayList<String>(); if (str == null) { return values; } StringTokenizer tokenizer = new StringTokenizer(str, ","); values = new ArrayList<String>(); while (tokenizer.hasMoreTokens()) { values.add(tokenizer.nextToken()); } return values; } /** * Returns an arraylist of strings. * * @param str * the comma seperated string values * @return the arraylist of the comma seperated string values */ public static String[] getStrings(String str) { Collection<String> values = getStringCollection(str); if (values.size() == 0) { return null; } return values.toArray(new String[values.size()]); } /** * Given an array of strings, return a comma-separated list of its elements. * * @param strs * Array of strings * @return Empty string if strs.length is 0, comma separated list of strings otherwise */ public static String arrayToString(String[] strs) { if (strs.length == 0) { return ""; } StringBuffer sbuf = new StringBuffer(); sbuf.append(strs[0]); for (int idx = 1; idx < strs.length; idx++) { sbuf.append(","); sbuf.append(strs[idx]); } return sbuf.toString(); } } private static final Log LOG = LogFactory.getLog(Configuration.class); private boolean quietmode = true; /** * List of configuration resources. */ private ArrayList<Object> resources = new ArrayList<Object>(); /** * List of configuration parameters marked <b>final</b>. */ private Set<String> finalParameters = new HashSet<String>(); private boolean loadDefaults = true; /** * Configurtion objects */ private static final WeakHashMap<Configuration, Object> REGISTRY = new WeakHashMap<Configuration, Object>(); /** * List of default Resources. Resources are loaded in the order of the list entries */ private static final CopyOnWriteArrayList<String> defaultResources = new CopyOnWriteArrayList<String>(); private Properties properties; private Properties overlay; private ClassLoader classLoader; { classLoader = Thread.currentThread().getContextClassLoader(); if (classLoader == null) { classLoader = Configuration.class.getClassLoader(); } } /** * Configuration? */ public Configuration() { this(true); } /** * Configuration????{@code loadDefaults} false?? * * @param loadDefaults * ?? */ public Configuration(boolean loadDefaults) { this.loadDefaults = loadDefaults; synchronized (Configuration.class) { REGISTRY.put(this, null); } } /** * Configuration? * * @param other * ?? */ @SuppressWarnings({ "unchecked", "rawtypes" }) public Configuration(Configuration other) { this.resources = (ArrayList) other.resources.clone(); synchronized (other) { if (other.properties != null) { this.properties = (Properties) other.properties.clone(); } if (other.overlay != null) { this.overlay = (Properties) other.overlay.clone(); } } this.finalParameters = new HashSet<String>(other.finalParameters); synchronized (Configuration.class) { REGISTRY.put(this, null); } } protected void checkState(String name) { } /** * ????? * * @param name * ???classpath */ public static synchronized void addDefaultResource(String name) { if (!defaultResources.contains(name)) { defaultResources.add(name); for (Configuration conf : REGISTRY.keySet()) { if (conf.loadDefaults) { conf.reloadConfiguration(); } } } } /** * ? * * ????? <a href="#Final">final</a>. * * @param name * ???classpath */ @Survey public void addResource(String name) { addResourceObject(name); } /** * ? * * ????? <a href="#Final">final</a>. * * @param url * ?url??url?? */ @Survey public void addResource(URL url) { addResourceObject(url); } /** * ? * * ????? <a href="#Final">final</a>. * * @param in * ?? */ @Survey public void addResource(InputStream in) { addResourceObject(in); } /** * ???{@link Configuration} * * ???? */ public synchronized void reloadConfiguration() { properties = null; // trigger reload finalParameters.clear(); // clear site-limits } private synchronized void addResourceObject(Object resource) { resources.add(resource); // add to resources reloadConfiguration(); } private static Pattern varPat = Pattern.compile("\\$\\{[^\\}\\$\u0020]+\\}"); private static int MAX_SUBST = 20; private String substituteVars(String expr) { if (expr == null) { return null; } Matcher match = varPat.matcher(""); String eval = expr; for (int s = 0; s < MAX_SUBST; s++) { match.reset(eval); if (!match.find()) { return eval; } String var = match.group(); var = var.substring(2, var.length() - 1); // remove ${ .. } String val = null; try { val = System.getProperty(var); } catch (SecurityException se) { LOG.warn("No permission to get system property: " + var); } if (val == null) { val = getRaw(var); } if (val == null) { return eval; // return literal ${var}: var is unbound } // substitute eval = eval.substring(0, match.start()) + val + eval.substring(match.end()); } throw new IllegalStateException("Variable substitution depth too large: " + MAX_SUBST + " " + expr); } /** * ??<code>name</code>?<code>null</code>??? * * @param name * ??? * @return ?<code>name</code>, ?<code>null</code> */ public String get(String name) { return substituteVars(getProps().getProperty(name)); } /** * ??<code>name</code>??? * * @param name * ??? * @return ?<code>name</code>, ?<code>null</code> */ public String getRaw(String name) { return getProps().getProperty(name); } /** * ?<code>name</code> <code>value</code> * * @param name * ??? * @param value * ? */ public void set(String name, String value) { checkState(name); getOverlay().setProperty(name, value); getProps().setProperty(name, value); } /** * ?? * * @param name * ??? * @param value * ? */ public void setIfUnset(String name, String value) { if (get(name) == null) { set(name, value); } } private synchronized Properties getOverlay() { if (overlay == null) { overlay = new Properties(); } return overlay; } /** * ??<code>name</code>?<code>defaultValue</code> * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>defaultValue</code> */ public String get(String name, String defaultValue) { return substituteVars(getProps().getProperty(name, defaultValue)); } /** * ??<code>name</code>?<code>defaultValue</code> * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>defaultValue</code> */ public int getInt(String name, int defaultValue) { String valueString = get(name); if (valueString == null) { return defaultValue; } try { String hexString = getHexDigits(valueString); if (hexString != null) { return Integer.parseInt(hexString, 16); } return Integer.parseInt(valueString); } catch (NumberFormatException e) { return defaultValue; } } /** * ? <code>name</code> * * @param name * ??? * @param value * ? */ public void setInt(String name, int value) { set(name, Integer.toString(value)); } /** * ??<code>name</code>?<code>defaultValue</code> * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>defaultValue</code> */ public long getLong(String name, long defaultValue) { String valueString = get(name); if (valueString == null) { return defaultValue; } try { String hexString = getHexDigits(valueString); if (hexString != null) { return Long.parseLong(hexString, 16); } return Long.parseLong(valueString); } catch (NumberFormatException e) { return defaultValue; } } private String getHexDigits(String value) { boolean negative = false; String str = value; String hexString = null; if (value.startsWith("-")) { negative = true; str = value.substring(1); } if (str.startsWith("0x") || str.startsWith("0X")) { hexString = str.substring(2); if (negative) { hexString = "-" + hexString; } return hexString; } return null; } /** * ? <code>name</code> * * @param name * ??? * @param value * ? */ public void setLong(String name, long value) { set(name, Long.toString(value)); } /** * ??<code>name</code>?<code>defaultValue</code> * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>defaultValue</code> */ public float getFloat(String name, float defaultValue) { String valueString = get(name); if (valueString == null) { return defaultValue; } try { return Float.parseFloat(valueString); } catch (NumberFormatException e) { return defaultValue; } } /** * ? <code>name</code> * * @param name * ??? * @param value * ? */ public void setFloat(String name, float value) { set(name, Float.toString(value)); } /** * ??<code>name</code>?<code>defaultValue</code> * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>defaultValue</code> */ public boolean getBoolean(String name, boolean defaultValue) { String valueString = get(name); if ("true".equals(valueString)) { return true; } else if ("false".equals(valueString)) { return false; } else { return defaultValue; } } /** * ? <code>name</code> * * @param name * ??? * @param value * ? */ public void setBoolean(String name, boolean value) { set(name, Boolean.toString(value)); } /** * ? <code>name</code> * * @param name * ??? * @param value * ? */ public void setBooleanIfUnset(String name, boolean value) { setIfUnset(name, Boolean.toString(value)); } /** * ???"2-3,5,7-"','?'-'????? * ??235789... */ static class IntegerRanges { private static class Range { int start; int end; } List<Range> ranges = new ArrayList<Range>(); public IntegerRanges() { } public IntegerRanges(String newValue) { StringTokenizer itr = new StringTokenizer(newValue, ","); while (itr.hasMoreTokens()) { String rng = itr.nextToken().trim(); String[] parts = rng.split("-", 3); if (parts.length < 1 || parts.length > 2) { throw new IllegalArgumentException("integer range badly formed: " + rng); } Range r = new Range(); r.start = convertToInt(parts[0], 0); if (parts.length == 2) { r.end = convertToInt(parts[1], Integer.MAX_VALUE); } else { r.end = r.start; } if (r.start > r.end) { throw new IllegalArgumentException( "IntegerRange from " + r.start + " to " + r.end + " is invalid"); } ranges.add(r); } } /** * Convert a string to an int treating empty strings as the default value. * * @param value * the string value * @param defaultValue * the value for if the string is empty * @return the desired integer */ private static int convertToInt(String value, int defaultValue) { String trim = value.trim(); if (trim.length() == 0) { return defaultValue; } return Integer.parseInt(trim); } /** * Is the given value in the set of ranges * * @param value * the value to check * @return is the value in the ranges? */ public boolean isIncluded(int value) { for (Range r : ranges) { if (r.start <= value && value <= r.end) { return true; } } return false; } @Override public String toString() { StringBuffer result = new StringBuffer(); boolean first = true; for (Range r : ranges) { if (first) { first = false; } else { result.append(','); } result.append(r.start); result.append('-'); result.append(r.end); } return result.toString(); } } /** * ??<code>name</code>?<code>defaultValue</code> * * @param name * ??? * @param defaultValue * * @return ?<code>name</code> */ public IntegerRanges getRange(String name, String defaultValue) { return new IntegerRanges(get(name, defaultValue)); } /** * ??<code>name</code>??<code>null</code> * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>null</code> */ public Collection<String> getStringCollection(String name) { String valueString = get(name); return StringUtils.getStringCollection(valueString); } /** * ??<code>name</code>?<code>null</code> * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>null</code> */ public String[] getStrings(String name) { String valueString = get(name); return StringUtils.getStrings(valueString); } /** * ??<code>name</code>?<code>defaultValue</code> * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>defaultValue</code> */ public String[] getStrings(String name, String... defaultValue) { String valueString = get(name); if (valueString == null) { return defaultValue; } else { return StringUtils.getStrings(valueString); } } /** * ?<code>name</code> <code>value</code> * * @param name * ??? * @param values * ? */ public void setStrings(String name, String... values) { set(name, StringUtils.arrayToString(values)); } /** * ???Java * * @param name * ?? * @return * @throws ClassNotFoundException * ?Java */ public Class<?> getClassByName(String name) throws ClassNotFoundException { return Class.forName(name, false, classLoader); } /** * ??<code>name</code>?<code>defaultValue</code> RuntimeException * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>defaultValue</code> */ public Class<?>[] getClasses(String name, Class<?>... defaultValue) { String[] classnames = getStrings(name); if (classnames == null) { return defaultValue; } try { Class<?>[] classes = new Class<?>[classnames.length]; for (int i = 0; i < classnames.length; i++) { classes[i] = getClassByName(classnames[i]); } return classes; } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } /** * ??<code>name</code>?<code>defaultValue</code> RuntimeException * * @param name * ??? * @param defaultValue * * @return ?<code>name</code>?<code>defaultValue</code> */ public Class<?> getClass(String name, Class<?> defaultValue) { String valueString = get(name); if (valueString == null) { return defaultValue; } try { return getClassByName(valueString); } catch (ClassNotFoundException e) { throw new RuntimeException("ODPS-0730001: ClassNotFoundException - " + e.getMessage()); } } /** * ??<code>name</code>? <code>xface</code> * * ?<code>defaultValue</code> * * @param name * ??? * @param defaultValue * * @param xface * ? * @return ?<code>name</code>, ? <code>defaultValue</code> */ public <U> Class<? extends U> getClass(String name, Class<? extends U> defaultValue, Class<U> xface) { try { Class<?> theClass = getClass(name, defaultValue); if (theClass != null && !xface.isAssignableFrom(theClass)) { throw new RuntimeException(theClass + " not " + xface.getName()); } else if (theClass != null) { return theClass.asSubclass(xface); } else { return null; } } catch (Exception e) { throw new RuntimeException(e); } } /** * <code>name</code>?? <code>xface</code> * * @param name * ??? * @param theClass * ? * @param xface * ? */ public void setClass(String name, Class<?> theClass, Class<?> xface) { if (!xface.isAssignableFrom(theClass)) { throw new RuntimeException(theClass + " not " + xface.getName()); } set(name, theClass.getName()); } /** * ??? <i>dirsProp</i> ? <i>path</i>. <i>dirsProp</i> ?, * * ? * * @param dirsProp * directory in which to locate the file. * @param path * file-path. * @return local file under the directory with the given path. */ public File getFile(String dirsProp, String path) throws IOException { String[] dirs = getStrings(dirsProp); int hashCode = path.hashCode(); for (int i = 0; i < dirs.length; i++) { // try each local dir int index = (hashCode + i & Integer.MAX_VALUE) % dirs.length; File file = new File(dirs[index], path); File dir = file.getParentFile(); if (dir.exists() || dir.mkdirs()) { return file; } } throw new IOException("No valid local directories in property: " + dirsProp); } /** * ??{@link URL} * * @param name * ??? * @return ?{@link URL} */ public URL getResource(String name) { return classLoader.getResource(name); } /** * ?? {@link InputStream} * * @param name * ??? * @return ?{@link InputStream} */ public InputStream getConfResourceAsInputStream(String name) { try { URL url = getResource(name); if (url == null) { LOG.info(name + " not found"); return null; } else { LOG.info("found resource " + name + " at " + url); } return url.openStream(); } catch (Exception e) { return null; } } /** * ?? {@link Reader} * * @param name * ??? * @return ?{@link Reader} */ public Reader getConfResourceAsReader(String name) { try { URL url = getResource(name); if (url == null) { LOG.info(name + " not found"); return null; } else { LOG.info("found resource " + name + " at " + url); } return new InputStreamReader(url.openStream()); } catch (Exception e) { return null; } } private synchronized Properties getProps() { if (properties == null) { properties = new Properties(); loadResources(properties, resources, quietmode); if (overlay != null) { properties.putAll(overlay); } } return properties; } /** * ? * * @return ? */ public int size() { return getProps().size(); } /** * ? */ public void clear() { getProps().clear(); getOverlay().clear(); } /** * ??key/valuekeyvaluestring * * @return ? */ public Iterator<Map.Entry<String, String>> iterator() { // Get a copy of just the string to string pairs. After the old object // methods that allow non-strings to be put into configurations are // removed, // we could replace properties with a Map<String,String> and get rid of // this // code. Map<String, String> result = new HashMap<String, String>(); for (Map.Entry<Object, Object> item : getProps().entrySet()) { if (item.getKey() instanceof String && item.getValue() instanceof String) { result.put((String) item.getKey(), (String) item.getValue()); } } return result.entrySet().iterator(); } @SuppressWarnings("rawtypes") private void loadResources(Properties properties, ArrayList resources, boolean quiet) { if (loadDefaults) { for (String resource : defaultResources) { loadResource(properties, resource, quiet); } } for (Object resource : resources) { loadResource(properties, resource, quiet); } } private void loadResource(Properties properties, Object name, boolean quiet) { try { DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); // ignore all comments inside the xml file docBuilderFactory.setIgnoringComments(true); // allow includes in the xml file docBuilderFactory.setNamespaceAware(true); try { docBuilderFactory.setXIncludeAware(true); } catch (UnsupportedOperationException e) { LOG.error("Failed to set setXIncludeAware(true) for parser " + docBuilderFactory + ":" + e, e); } DocumentBuilder builder = docBuilderFactory.newDocumentBuilder(); Document doc = null; Element root = null; if (name instanceof URL) { // an URL resource URL url = (URL) name; if (url != null) { if (!quiet) { LOG.info("parsing " + url); } doc = builder.parse(url.toString()); } } else if (name instanceof String) { // a CLASSPATH resource URL url = getResource((String) name); if (url != null) { if (!quiet) { LOG.info("parsing " + url); } doc = builder.parse(url.toString()); } } else if (name instanceof InputStream) { try { doc = builder.parse((InputStream) name); } finally { ((InputStream) name).close(); } } else if (name instanceof Element) { root = (Element) name; } if (doc == null && root == null) { if (quiet) { return; } throw new RuntimeException(name + " not found"); } if (root == null) { root = doc.getDocumentElement(); } if (!"configuration".equals(root.getTagName())) { LOG.fatal("bad conf file: top-level element not <configuration>"); } NodeList props = root.getChildNodes(); for (int i = 0; i < props.getLength(); i++) { Node propNode = props.item(i); if (!(propNode instanceof Element)) { continue; } Element prop = (Element) propNode; if ("configuration".equals(prop.getTagName())) { loadResource(properties, prop, quiet); continue; } if (!"property".equals(prop.getTagName())) { LOG.warn("bad conf file: element not <property>"); } NodeList fields = prop.getChildNodes(); String attr = null; String value = null; boolean finalParameter = false; for (int j = 0; j < fields.getLength(); j++) { Node fieldNode = fields.item(j); if (!(fieldNode instanceof Element)) { continue; } Element field = (Element) fieldNode; if ("name".equals(field.getTagName()) && field.hasChildNodes()) { attr = ((Text) field.getFirstChild()).getData().trim(); } if ("value".equals(field.getTagName()) && field.hasChildNodes()) { value = ((Text) field.getFirstChild()).getData(); } if ("final".equals(field.getTagName()) && field.hasChildNodes()) { finalParameter = "true".equals(((Text) field.getFirstChild()).getData()); } } // Ignore this parameter if it has already been marked as // 'final' if (attr != null && value != null) { if (!finalParameters.contains(attr)) { properties.setProperty(attr, value); if (finalParameter) { finalParameters.add(attr); } } else { LOG.warn(name + ":a attempt to override final parameter: " + attr + "; Ignoring."); } } } } catch (IOException e) { LOG.fatal("error parsing conf file: " + e); throw new RuntimeException(e); } catch (DOMException e) { LOG.fatal("error parsing conf file: " + e); throw new RuntimeException(e); } catch (SAXException e) { LOG.fatal("error parsing conf file: " + e); throw new RuntimeException(e); } catch (ParserConfigurationException e) { LOG.fatal("error parsing conf file: " + e); throw new RuntimeException(e); } } /** * ?? {@link OutputStream} * * @param out * {@link OutputStream} */ @SuppressWarnings("rawtypes") public void writeXml(OutputStream out) throws IOException { Properties properties = getProps(); try { Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); Element conf = doc.createElement("configuration"); doc.appendChild(conf); conf.appendChild(doc.createTextNode("\n")); for (Enumeration e = properties.keys(); e.hasMoreElements();) { String name = (String) e.nextElement(); Object object = properties.get(name); String value = null; if (object instanceof String) { value = (String) object; } else { continue; } Element propNode = doc.createElement("property"); conf.appendChild(propNode); Element nameNode = doc.createElement("name"); nameNode.appendChild(doc.createTextNode(name)); propNode.appendChild(nameNode); Element valueNode = doc.createElement("value"); valueNode.appendChild(doc.createTextNode(value)); propNode.appendChild(valueNode); conf.appendChild(doc.createTextNode("\n")); } DOMSource source = new DOMSource(doc); StreamResult result = new StreamResult(out); TransformerFactory transFactory = TransformerFactory.newInstance(); Transformer transformer = transFactory.newTransformer(); transformer.transform(source, result); } catch (Exception e) { throw new RuntimeException(e); } } /** * ?{@link ClassLoader} * * @return {@link ClassLoader} */ public ClassLoader getClassLoader() { return classLoader; } /** * {@link ClassLoader} * * @param classLoader * {@link ClassLoader} */ public void setClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } @Override public String toString() { StringBuffer sb = new StringBuffer(); sb.append("Configuration: "); if (loadDefaults) { toString(defaultResources, sb); if (resources.size() > 0) { sb.append(", "); } } toString(resources, sb); return sb.toString(); } @SuppressWarnings("rawtypes") private void toString(List resources, StringBuffer sb) { ListIterator i = resources.listIterator(); while (i.hasNext()) { if (i.nextIndex() != 0) { sb.append(", "); } sb.append(i.next()); } } /** * quiet?quiet??? * * @param quietmode * <code>true</code> <code>false</code> */ public synchronized void setQuietMode(boolean quietmode) { this.quietmode = quietmode; } /** * {@link DataInput} ? */ @Override public void readFields(DataInput in) throws IOException { clear(); int size = WritableUtils.readVInt(in); for (int i = 0; i < size; ++i) { set(com.aliyun.odps.io.Text.readString(in), com.aliyun.odps.io.Text.readString(in)); } } /** * {@link DataOutput} */ @Override public void write(DataOutput out) throws IOException { Properties props = getProps(); WritableUtils.writeVInt(out, props.size()); for (Map.Entry<Object, Object> item : props.entrySet()) { com.aliyun.odps.io.Text.writeString(out, (String) item.getKey()); com.aliyun.odps.io.Text.writeString(out, (String) item.getValue()); } } }