| /* |
| * 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.openjpa.slice.jdbc; |
| |
| import java.lang.reflect.Method; |
| import java.security.AccessController; |
| import java.sql.Connection; |
| import java.sql.SQLException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| |
| import javax.sql.DataSource; |
| import javax.sql.XADataSource; |
| |
| import org.apache.openjpa.conf.OpenJPAConfiguration; |
| import org.apache.openjpa.jdbc.conf.JDBCConfiguration; |
| import org.apache.openjpa.jdbc.conf.JDBCConfigurationImpl; |
| import org.apache.openjpa.jdbc.schema.DataSourceFactory; |
| import org.apache.openjpa.lib.conf.BooleanValue; |
| import org.apache.openjpa.lib.conf.ConfigurationProvider; |
| import org.apache.openjpa.lib.conf.PluginValue; |
| import org.apache.openjpa.lib.conf.StringListValue; |
| import org.apache.openjpa.lib.conf.StringValue; |
| import org.apache.openjpa.lib.jdbc.DecoratingDataSource; |
| import org.apache.openjpa.lib.jdbc.DelegatingDataSource; |
| import org.apache.openjpa.lib.log.Log; |
| import org.apache.openjpa.lib.log.LogFactory; |
| import org.apache.openjpa.lib.util.J2DoPrivHelper; |
| import org.apache.openjpa.lib.util.Localizer; |
| import org.apache.openjpa.meta.ClassMetaData; |
| import org.apache.openjpa.meta.MetaDataRepository; |
| import org.apache.openjpa.slice.DistributedBrokerImpl; |
| import org.apache.openjpa.slice.DistributionPolicy; |
| import org.apache.openjpa.slice.FinderTargetPolicy; |
| import org.apache.openjpa.slice.ProductDerivation; |
| import org.apache.openjpa.slice.QueryTargetPolicy; |
| import org.apache.openjpa.slice.ReplicationPolicy; |
| import org.apache.openjpa.slice.Slice; |
| import org.apache.openjpa.util.UserException; |
| |
| /** |
| * A specialized configuration embodies a set of Slice configurations. |
| * The original configuration properties are analyzed to create a set of |
| * Slice specific properties with defaulting rules. |
| * |
| * @author Pinaki Poddar |
| * |
| */ |
| public class DistributedJDBCConfigurationImpl extends JDBCConfigurationImpl |
| implements DistributedJDBCConfiguration { |
| |
| private final List<Slice> _slices = new ArrayList<Slice>(); |
| private Slice _master; |
| |
| private DistributedDataSource virtualDataSource; |
| |
| protected BooleanValue lenientPlugin; |
| protected StringValue masterPlugin; |
| protected StringListValue namesPlugin; |
| public PluginValue distributionPolicyPlugin; |
| public PluginValue replicationPolicyPlugin; |
| public PluginValue queryTargetPolicyPlugin; |
| public PluginValue finderTargetPolicyPlugin; |
| public StringListValue replicatedTypesPlugin; |
| |
| private ReplicatedTypeRepository _replicationRepos; |
| |
| public static final String DOT = "."; |
| public static final String REGEX_DOT = "\\."; |
| public static final String PREFIX_SLICE = ProductDerivation.PREFIX_SLICE + DOT; |
| public static final String PREFIX_OPENJPA = "openjpa."; |
| private static Localizer _loc = Localizer.forPackage(DistributedJDBCConfigurationImpl.class); |
| |
| /** |
| * Create a configuration and declare the plug-ins. |
| */ |
| public DistributedJDBCConfigurationImpl() { |
| super(true, // load derivations |
| false); // load globals |
| brokerPlugin.setString(DistributedBrokerImpl.class.getName()); |
| |
| distributionPolicyPlugin = addPlugin(PREFIX_SLICE + "DistributionPolicy", true); |
| distributionPolicyPlugin.setAlias("random", DistributionPolicy.Default.class.getName()); |
| distributionPolicyPlugin.setDefault("random"); |
| distributionPolicyPlugin.setString("random"); |
| distributionPolicyPlugin.setDynamic(true); |
| |
| replicationPolicyPlugin = addPlugin(PREFIX_SLICE + "ReplicationPolicy", true); |
| replicationPolicyPlugin.setAlias("all", ReplicationPolicy.Default.class.getName()); |
| replicationPolicyPlugin.setDefault("all"); |
| replicationPolicyPlugin.setString("all"); |
| replicationPolicyPlugin.setDynamic(true); |
| |
| queryTargetPolicyPlugin = addPlugin(PREFIX_SLICE + "QueryTargetPolicy", true); |
| queryTargetPolicyPlugin.setDynamic(true); |
| |
| finderTargetPolicyPlugin = addPlugin(PREFIX_SLICE + "FinderTargetPolicy", true); |
| finderTargetPolicyPlugin.setDynamic(true); |
| |
| replicatedTypesPlugin = new StringListValue(PREFIX_SLICE + "ReplicatedTypes"); |
| addValue(replicatedTypesPlugin); |
| |
| lenientPlugin = addBoolean(PREFIX_SLICE + "Lenient"); |
| lenientPlugin.setDefault("true"); |
| |
| masterPlugin = addString(PREFIX_SLICE + "Master"); |
| namesPlugin = addStringList(PREFIX_SLICE + "Names"); |
| } |
| |
| /** |
| * Configure itself as well as underlying slices. |
| * |
| */ |
| public DistributedJDBCConfigurationImpl(ConfigurationProvider cp) { |
| this(); |
| cp.setInto(this); |
| setDiagnosticContext(this); |
| } |
| |
| private void setDiagnosticContext(OpenJPAConfiguration conf) { |
| LogFactory logFactory = conf.getLogFactory(); |
| try { |
| Method setter = AccessController.doPrivileged(J2DoPrivHelper. |
| getDeclaredMethodAction(logFactory.getClass(), |
| "setDiagnosticContext", new Class[]{String.class})); |
| setter.invoke(logFactory, conf.getId()); |
| } catch (Throwable t) { |
| // no contextual logging |
| } |
| } |
| |
| /** |
| * Gets the name of the active slices. |
| */ |
| public List<String> getActiveSliceNames() { |
| List<String> result = new ArrayList<String>(); |
| for (Slice slice : _slices) { |
| if (slice.isActive() && !result.contains(slice.getName())) |
| result.add(slice.getName()); |
| } |
| return result; |
| } |
| |
| /** |
| * Gets the name of the available slices. |
| */ |
| public List<String> getAvailableSliceNames() { |
| List<String> result = new ArrayList<String>(); |
| for (Slice slice : _slices) |
| result.add(slice.getName()); |
| return result; |
| } |
| |
| /** |
| * Gets the slices of given status. Null returns all irrespective of status. |
| */ |
| public List<Slice> getSlices(Slice.Status...statuses) { |
| if (statuses == null) |
| return _slices == null ? Collections.EMPTY_LIST : Collections.unmodifiableList(_slices); |
| List<Slice> result = new ArrayList<Slice>(); |
| for (Slice slice:_slices) { |
| for (Slice.Status status:statuses) |
| if (slice.getStatus().equals(status)) |
| result.add(slice); |
| } |
| return result; |
| } |
| |
| |
| public Slice getSlice(String name) { |
| return getSlice(name, false); |
| } |
| |
| /** |
| * Get the Slice of the given slice. |
| * |
| * @param mustExist if true an exception if raised if the given slice name |
| * is not a valid slice. |
| */ |
| public Slice getSlice(String name, boolean mustExist) { |
| for (Slice slice : _slices) |
| if (slice.getName().equals(name)) |
| return slice; |
| if (mustExist) { |
| throw new UserException(_loc.get("slice-not-found", name, |
| getActiveSliceNames())); |
| } |
| return null; |
| } |
| |
| public DistributionPolicy getDistributionPolicyInstance() { |
| if (distributionPolicyPlugin.get() == null) { |
| distributionPolicyPlugin.instantiate(DistributionPolicy.class, |
| this, true); |
| } |
| return (DistributionPolicy) distributionPolicyPlugin.get(); |
| } |
| |
| public String getDistributionPolicy() { |
| if (distributionPolicyPlugin.get() == null) { |
| distributionPolicyPlugin.instantiate(DistributionPolicy.class, |
| this, true); |
| } |
| return distributionPolicyPlugin.getString(); |
| } |
| |
| public void setDistributionPolicyInstance(DistributionPolicy policy) { |
| distributionPolicyPlugin.set(policy); |
| } |
| |
| public void setDistributionPolicy(String policy) { |
| distributionPolicyPlugin.setString(policy); |
| } |
| |
| public ReplicationPolicy getReplicationPolicyInstance() { |
| if (replicationPolicyPlugin.get() == null) { |
| replicationPolicyPlugin.instantiate(ReplicationPolicy.class, |
| this, true); |
| } |
| return (ReplicationPolicy) replicationPolicyPlugin.get(); |
| } |
| |
| public String getReplicationPolicy() { |
| if (replicationPolicyPlugin.get() == null) { |
| replicationPolicyPlugin.instantiate(ReplicationPolicy.class, |
| this, true); |
| } |
| return replicationPolicyPlugin.getString(); |
| } |
| |
| public void setReplicationPolicyInstance(ReplicationPolicy policy) { |
| replicationPolicyPlugin.set(policy); |
| } |
| |
| public void setReplicationPolicy(String policy) { |
| replicationPolicyPlugin.setString(policy); |
| } |
| |
| public QueryTargetPolicy getQueryTargetPolicyInstance() { |
| if (queryTargetPolicyPlugin.get() == null) { |
| queryTargetPolicyPlugin.instantiate(QueryTargetPolicy.class, |
| this, true); |
| } |
| return (QueryTargetPolicy) queryTargetPolicyPlugin.get(); |
| } |
| |
| public String getQueryTargetPolicy() { |
| if (queryTargetPolicyPlugin.get() == null) { |
| queryTargetPolicyPlugin.instantiate(QueryTargetPolicy.class, |
| this, true); |
| } |
| return queryTargetPolicyPlugin.getString(); |
| } |
| |
| public void setQueryTargetPolicyInstance(QueryTargetPolicy policy) { |
| queryTargetPolicyPlugin.set(policy); |
| } |
| |
| public void setQueryTargetPolicy(String policy) { |
| queryTargetPolicyPlugin.setString(policy); |
| } |
| |
| public FinderTargetPolicy getFinderTargetPolicyInstance() { |
| if (finderTargetPolicyPlugin.get() == null) { |
| finderTargetPolicyPlugin.instantiate(FinderTargetPolicy.class, |
| this, true); |
| } |
| return (FinderTargetPolicy) finderTargetPolicyPlugin.get(); |
| } |
| |
| public String getFinderTargetPolicy() { |
| if (finderTargetPolicyPlugin.get() == null) { |
| finderTargetPolicyPlugin.instantiate(FinderTargetPolicy.class, |
| this, true); |
| } |
| return finderTargetPolicyPlugin.getString(); |
| } |
| |
| public void setFinderTargetPolicyInstance(FinderTargetPolicy policy) { |
| finderTargetPolicyPlugin.set(policy); |
| } |
| |
| public void setFinderTargetPolicy(String policy) { |
| finderTargetPolicyPlugin.setString(policy); |
| } |
| |
| public DistributedDataSource getConnectionFactory() { |
| if (virtualDataSource == null) { |
| virtualDataSource = createDistributedDataStore(); |
| DataSourceFactory.installDBDictionary( |
| getDBDictionaryInstance(), virtualDataSource, this, false); |
| } |
| return virtualDataSource; |
| } |
| |
| public boolean isLenient() { |
| return lenientPlugin.get(); |
| } |
| |
| public void setLenient(boolean lenient) { |
| lenientPlugin.set(lenient); |
| } |
| |
| public void setMaster(String master) { |
| masterPlugin.set(master); |
| } |
| |
| /** |
| * Gets the master slice. |
| */ |
| public Slice getMasterSlice() { |
| if (_master == null) { |
| String value = masterPlugin.get(); |
| if (value == null) { |
| _master = _slices.get(0); |
| } else { |
| _master = getSlice(value, true); |
| } |
| } |
| return _master; |
| } |
| |
| /** |
| * Create a virtual DistributedDataSource as a composite of individual |
| * slices as per configuration, optionally ignoring slices that can not be |
| * connected. |
| */ |
| private DistributedDataSource createDistributedDataStore() { |
| List<DataSource> dataSources = new ArrayList<DataSource>(); |
| boolean isXA = true; |
| for (Slice slice : _slices) { |
| try { |
| DataSource ds = createDataSource(slice); |
| dataSources.add(ds); |
| isXA &= isXACompliant(ds); |
| } catch (Throwable ex) { |
| handleBadConnection(isLenient(), slice, ex); |
| } |
| } |
| if (dataSources.isEmpty()) |
| throw new UserException(_loc.get("no-slice")); |
| DistributedDataSource result = new DistributedDataSource(dataSources); |
| return result; |
| } |
| |
| DataSource createDataSource(Slice slice) throws Exception { |
| JDBCConfiguration conf = (JDBCConfiguration)slice.getConfiguration(); |
| DataSource ds = (DataSource)conf.getConnectionFactory(); |
| if (ds == null) { |
| Log log = conf.getConfigurationLog(); |
| String url = getConnectionInfo(conf); |
| if (log.isInfoEnabled()) |
| log.info(_loc.get("slice-connect", slice, url)); |
| ds = DataSourceFactory.newDataSource(conf, false); |
| DecoratingDataSource dds = new DecoratingDataSource(ds); |
| ds = DataSourceFactory.installDBDictionary( |
| conf.getDBDictionaryInstance(), dds, conf, false); |
| } |
| verifyDataSource(slice, ds, conf); |
| |
| return ds; |
| } |
| |
| String getConnectionInfo(OpenJPAConfiguration conf) { |
| String result = conf.getConnectionURL(); |
| if (result == null) { |
| result = conf.getConnectionDriverName(); |
| String props = conf.getConnectionProperties(); |
| if (props != null) |
| result += "(" + props + ")"; |
| } |
| return result; |
| } |
| |
| boolean isXACompliant(DataSource ds) { |
| if (ds instanceof DelegatingDataSource) |
| return ((DelegatingDataSource) ds).getInnermostDelegate() |
| instanceof XADataSource; |
| return ds instanceof XADataSource; |
| } |
| |
| /** |
| * Verify that a connection can be established to the given slice. If |
| * connection can not be established then slice is set to INACTIVE state. |
| */ |
| private boolean verifyDataSource(Slice slice, DataSource ds, |
| JDBCConfiguration conf) { |
| Connection con = null; |
| try { |
| con = ds.getConnection(conf.getConnectionUserName(), |
| conf.getConnectionPassword()); |
| slice.setStatus(Slice.Status.ACTIVE); |
| if (con == null) { |
| slice.setStatus(Slice.Status.INACTIVE); |
| return false; |
| } |
| return true; |
| } catch (SQLException ex) { |
| slice.setStatus(Slice.Status.INACTIVE); |
| return false; |
| } finally { |
| if (con != null) |
| try { |
| con.close(); |
| } catch (SQLException ex) { |
| // ignore |
| } |
| } |
| } |
| |
| /** |
| * Either throw a user exception or add the configuration to the given list, |
| * based on <code>isLenient</code>. |
| */ |
| private void handleBadConnection(boolean isLenient, Slice slice, Throwable ex) { |
| OpenJPAConfiguration conf = slice.getConfiguration(); |
| String url = conf.getConnectionURL(); |
| Log log = conf.getConfigurationLog(); |
| if (isLenient) { |
| if (ex != null) { |
| log.warn(_loc.get("slice-connect-known-warn", slice, url, ex.getCause())); |
| } else { |
| log.warn(_loc.get("slice-connect-warn", slice, url)); |
| } |
| } else if (ex != null) { |
| throw new UserException(_loc.get("slice-connect-known-error", slice, url, ex), ex.getCause()); |
| } else { |
| throw new UserException(_loc.get("slice-connect-error", slice, url)); |
| } |
| } |
| |
| /** |
| * Create a new Slice of given name and given properties. |
| * |
| * @param key name of the slice to be created |
| * @param original a set of properties. |
| * @return a newly configured slice |
| */ |
| private Slice newSlice(String key, Map original) { |
| JDBCConfiguration child = new JDBCConfigurationImpl(); |
| child.fromProperties(createSliceProperties(original, key)); |
| child.setId(getId()+DOT+key); |
| setDiagnosticContext(child); |
| child.setMappingDefaults(this.getMappingDefaultsInstance()); |
| child.setDataCacheManager(this.getDataCacheManagerInstance()); |
| child.setMetaDataRepository(this.getMetaDataRepositoryInstance()); |
| Slice slice = new Slice(key, child); |
| Log log = getConfigurationLog(); |
| if (log.isTraceEnabled()) |
| log.trace(_loc.get("slice-configuration", key, child |
| .toProperties(false))); |
| return slice; |
| } |
| |
| /** |
| * Finds the slices. If <code>openjpa.slice.Names</code> property is |
| * specified then the slices are ordered in the way they are listed. |
| * Otherwise scans all available slices by looking for property of the form |
| * <code>openjpa.slice.XYZ.abc</code> where <code>XYZ</code> is the slice |
| * identifier and <code>abc</code> is any openjpa property name. The slices |
| * are then ordered alphabetically by their identifier. |
| */ |
| private List<String> findSlices(Map p) { |
| List<String> sliceNames = new ArrayList<String>(); |
| |
| Log log = getConfigurationLog(); |
| String key = namesPlugin.getProperty(); |
| boolean explicit = p.containsKey(key); |
| if (explicit) { |
| String[] values = p.get(key).toString().split("\\,"); |
| for (String name:values) |
| if (!sliceNames.contains(name.trim())) |
| sliceNames.add(name.trim()); |
| } else { |
| if (log.isWarnEnabled()) |
| log.warn(_loc.get("no-slice-names", key)); |
| sliceNames = scanForSliceNames(p); |
| Collections.sort(sliceNames); |
| } |
| if (log.isInfoEnabled()) { |
| log.info(_loc.get("slice-available", sliceNames)); |
| } |
| return sliceNames; |
| } |
| |
| /** |
| * Scan the given map for slice-specific property of the form |
| * <code>openjpa.slice.XYZ.abc</code> (while ignoring |
| * <code>openjpa.slice.XYZ</code> as they refer to slice-wide property) |
| * to determine the names of all available slices. |
| */ |
| private List<String> scanForSliceNames(Map p) { |
| List<String> sliceNames = new ArrayList<String>(); |
| for (Object o : p.keySet()) { |
| String key = o.toString(); |
| if (key.startsWith(PREFIX_SLICE) && getPartCount(key) > 3) { |
| String sliceName = |
| chopTail(chopHead(o.toString(), PREFIX_SLICE), DOT); |
| if (!sliceNames.contains(sliceName)) |
| sliceNames.add(sliceName); |
| } |
| } |
| return sliceNames; |
| } |
| |
| private static int getPartCount(String s) { |
| return (s == null) ? 0 : s.split(REGEX_DOT).length; |
| } |
| |
| private static String chopHead(String s, String head) { |
| if (s.startsWith(head)) |
| return s.substring(head.length()); |
| return s; |
| } |
| |
| private static String chopTail(String s, String tail) { |
| int i = s.lastIndexOf(tail); |
| if (i == -1) |
| return s; |
| return s.substring(0, i); |
| } |
| |
| /** |
| * Creates given <code>slice</code> specific configuration properties from |
| * given <code>original</code> key-value map. The rules are |
| * <LI> if key begins with <code>"openjpa.slice.XXX."</code> where |
| * <code>XXX</code> is the given slice name, then replace |
| * <code>"openjpa.slice.XXX.</code> with <code>openjpa.</code>. |
| * <LI>if key begins with <code>"openjpa.slice."</code> but not with |
| * <code>"openjpa.slice.XXX."</code>, then ignore i.e. any property of other |
| * slices or global slice property e.g. |
| * <code>openjpa.slice.DistributionPolicy</code> |
| * <li>if key starts with <code>"openjpa."</code> and a corresponding |
| * <code>"openjpa.slice.XXX."</code> property does not exist, then use this as |
| * default property |
| * <li>property with any other prefix is simply copied |
| * |
| */ |
| Map createSliceProperties(Map original, String slice) { |
| Map result = new Properties(); |
| String prefix = PREFIX_SLICE + slice + DOT; |
| for (Object o : original.keySet()) { |
| String key = o.toString(); |
| Object value = original.get(key); |
| if (value == null) |
| continue; |
| if (key.startsWith(prefix)) { |
| String newKey = PREFIX_OPENJPA + key.substring(prefix.length()); |
| result.put(newKey, value); |
| } else if (key.startsWith(PREFIX_SLICE)) { |
| // ignore keys that are in 'slice.' namespace but not this slice |
| } else if (key.startsWith(PREFIX_OPENJPA)) { |
| String newKey = prefix + key.substring(PREFIX_OPENJPA.length()); |
| if (!original.containsKey(newKey)) |
| result.put(key, value); |
| } else { // keys that are neither "openjpa" nor "slice" namespace |
| result.put(key, value); |
| } |
| } |
| return result; |
| } |
| |
| Slice addSlice(String name, Map newProps) { |
| String prefix = PREFIX_SLICE + DOT + name + DOT; |
| for (Object key : newProps.keySet()) { |
| if (!String.class.isInstance(key) |
| && key.toString().startsWith(prefix)) |
| throw new UserException(_loc.get("slice-add-wrong-key", key)); |
| } |
| Slice slice = getSlice(name); |
| if (slice != null) |
| throw new UserException(_loc.get("slice-exists", name)); |
| Map<String,String> original = super.toProperties(true); |
| original.putAll(newProps); |
| slice = newSlice(name, original); |
| _slices.add(slice); |
| try { |
| getConnectionFactory().addDataSource(createDataSource(slice)); |
| } catch (Exception ex) { |
| handleBadConnection(false, slice, ex); |
| return null; |
| } |
| return slice; |
| } |
| |
| /** |
| * Given the properties, creates a set of individual configurations. |
| */ |
| @Override |
| public void fromProperties(Map original) { |
| super.fromProperties(original); |
| setDiagnosticContext(this); |
| List<String> sliceNames = findSlices(original); |
| for (String name : sliceNames) { |
| Slice slice = newSlice(name, original); |
| _slices.add(slice); |
| } |
| } |
| |
| @Override |
| public DecoratingDataSource createConnectionFactory() { |
| if (virtualDataSource == null) { |
| virtualDataSource = createDistributedDataStore(); |
| } |
| return virtualDataSource; |
| } |
| |
| public boolean isReplicated(Class<?> cls) { |
| if (_replicationRepos == null) { |
| _replicationRepos = new ReplicatedTypeRepository(getMetaDataRepositoryInstance(), |
| Arrays.asList(replicatedTypesPlugin.get())); |
| } |
| return _replicationRepos.contains(cls); |
| } |
| |
| |
| /** |
| * A private repository of replicated types. |
| * |
| * @author Pinaki Poddar |
| * |
| */ |
| private static class ReplicatedTypeRepository { |
| private Set<Class<?>> _replicatedTypes = new HashSet<Class<?>>(); |
| private Set<Class<?>> _nonreplicatedTypes = new HashSet<Class<?>>(); |
| |
| |
| List<String> names; |
| MetaDataRepository repos; |
| |
| ReplicatedTypeRepository(MetaDataRepository repos, List<String> given) { |
| names = given; |
| this.repos = repos; |
| } |
| |
| boolean contains(Class<?> cls) { |
| if (_replicatedTypes.contains(cls)) |
| return true; |
| if (_nonreplicatedTypes.contains(cls)) |
| return false; |
| ClassMetaData meta = repos.getMetaData(cls, null, false); |
| if (meta == null) { |
| _nonreplicatedTypes.add(cls); |
| return false; |
| } |
| boolean replicated = names.contains(meta.getDescribedType().getName()); |
| if (replicated) { |
| _replicatedTypes.add(cls); |
| } else { |
| _nonreplicatedTypes.add(cls); |
| } |
| return replicated; |
| } |
| } |
| } |