OPENJPA-738 QueryCache Improvement

Committing patch provided by Sandhya Sturaga

git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@706481 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Catalina Wei 2008-10-21 00:23:29 +00:00
parent 16e7a11f81
commit e2388e9b3c
11 changed files with 821 additions and 19 deletions

View File

@ -18,12 +18,16 @@
*/
package org.apache.openjpa.datacache;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.event.RemoteCommitEvent;
@ -31,6 +35,7 @@ import org.apache.openjpa.event.RemoteCommitListener;
import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.concurrent.AbstractConcurrentEventManager;
import org.apache.openjpa.lib.util.concurrent.ConcurrentReferenceHashSet;
@ -63,34 +68,65 @@ public abstract class AbstractQueryCache
*/
protected Log log;
protected ConcurrentHashMap<String,Long> entityTimestampMap = null;
private boolean _closed = false;
protected String evictPolicy = "default";
public void initialize(DataCacheManager manager) {
if(evictPolicy.equalsIgnoreCase("timestamp")) {
entityTimestampMap = new ConcurrentHashMap<String,Long>();
// Get all persistence types to pre-load the entityTimestamp Map
Collection perTypes = conf.getMetaDataRepositoryInstance().
getPersistentTypeNames(false,
(ClassLoader) AccessController.doPrivileged(
J2DoPrivHelper.getContextClassLoaderAction()));
// Pre-load all the entity types into the HashMap to handle
// synchronization on the map efficiently
for (Object o : perTypes)
entityTimestampMap.put((String)o, new Long(0));
}
}
public void onTypesChanged(TypesChangedEvent ev) {
writeLock();
Collection keys = null;
try {
if (hasListeners())
fireEvent(ev);
keys = keySet();
} finally {
writeUnlock();
}
QueryKey qk;
List removes = null;
for (Iterator iter = keys.iterator(); iter.hasNext();) {
qk = (QueryKey) iter.next();
if (qk.changeInvalidatesQuery(ev.getTypes())) {
if (removes == null)
removes = new ArrayList();
removes.add(qk);
if (!evictPolicy.equalsIgnoreCase("timestamp")) {
try {
if (hasListeners())
fireEvent(ev);
keys = keySet();
} finally {
writeUnlock();
}
QueryKey qk;
List<QueryKey> removes = null;
for (Object o: keys) {
qk = (QueryKey) o;
if (qk.changeInvalidatesQuery(ev.getTypes())) {
if (removes == null)
removes = new ArrayList<QueryKey>();
removes.add(qk);
}
}
if (removes != null)
removeAllInternal(removes);
} else {
Collection changedTypes = ev.getTypes();
HashMap<String,Long> changedClasses =
new HashMap<String,Long>();
for (Object o: changedTypes) {
String name = ((Class) o).getName();
if(!changedClasses.containsKey(name))
changedClasses.put(name,
new Long(System.currentTimeMillis()));
}
// Now update entity timestamp map
updateEntityTimestampMap(changedClasses);
}
if (removes != null)
removeAllInternal(removes);
}
public QueryResult get(QueryKey key) {
@ -319,4 +355,50 @@ public abstract class AbstractQueryCache
protected Collection newListenerCollection() {
return new ConcurrentReferenceHashSet (ConcurrentReferenceHashSet.WEAK);
}
/**
* Sets the eviction policy for the query cache
* @param evictPolicy -- String value that specifies the eviction policy
*/
public void setEvictPolicy(String evictPolicy) {
this.evictPolicy = evictPolicy;
}
/**
* Returns the evictionPolicy for QueryCache
* @return -- returns a String value of evictPolicy attribute
*/
public String getEvictPolicy() {
return null;
}
/**
* Updates the entity timestamp map with the current time in milliseconds
* @param timestampMap -- a map that contains entityname and its last updated timestamp
*/
protected void updateEntityTimestampMap(Map<String,Long> timestampMap) {
if (entityTimestampMap != null)
entityTimestampMap.putAll(timestampMap);
}
/**
* Returns a list of timestamps in the form of Long objects
* which are the last updated time stamps for the given entities in the
* keylist.
* @param keyList -- List of entity names
* @return -- Returns a list that has the timestamp for the given entities
*/
public List<Long> getAllEntityTimestampFromMap(List<String> keyList) {
ArrayList<Long> tmval = null;
if (entityTimestampMap != null) {
for (String s: keyList) {
if (entityTimestampMap.containsKey(s)) {
if(tmval == null)
tmval = new ArrayList<Long>();
tmval.add(entityTimestampMap.get(s));
}
}
}
return tmval;
}
}

View File

@ -137,4 +137,11 @@ public class ConcurrentQueryCache
protected Collection keySet() {
return _cache.keySet ();
}
/**
* Returns the eviction policy of the query cache
*/
public String getEvictPolicy() {
return super.evictPolicy;
}
}

View File

@ -28,6 +28,7 @@ import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.apache.commons.collections.map.LinkedMap;
@ -119,10 +120,34 @@ public class QueryCacheStoreQuery
// get the cached data
QueryResult res = _cache.get(qk);
if (res == null)
return null;
return null;
if (res.isEmpty())
return Collections.EMPTY_LIST;
// this if block is invoked if the evictOnTimestamp is set to true
if (_cache instanceof AbstractQueryCache) {
AbstractQueryCache qcache = (AbstractQueryCache) _cache;
if (qcache.getEvictPolicy().equalsIgnoreCase("timestamp")) {
Set<String> classNames = qk.getAcessPathClassNames();
List<String> keyList = new ArrayList<String>();
keyList.addAll(classNames);
List<Long> timestamps =
qcache.getAllEntityTimestampFromMap(keyList);
long queryTS = res.getTimestamp();
if (timestamps != null) {
for (Long ts: timestamps) {
// if this is true we have to evict the query
// from cache
if (queryTS < ts) {
qcache.remove(qk);
return null;
}
}
}
}
}
int projs = getContext().getProjectionAliases().length;
if (projs == 0) {
// make sure the data cache contains the oids for the query result;
@ -572,6 +597,7 @@ public class QueryCacheStoreQuery
QueryResult res = null;
synchronized (this) {
res = new QueryResult(_qk, _data.values());
res.setTimestamp(System.currentTimeMillis());
}
_cache.put(_qk, res);
abortCaching();

View File

@ -200,6 +200,8 @@ public class QueryKey
// since the class change framework deals with least-derived types,
// record the least-derived access path types
meta = metas[i];
if (meta.getDataCache() != null)
accessPathClassNames.add(meta.getDescribedType().getName());
while (meta.getPCSuperclass() != null)
meta = meta.getPCSuperclassMetaData();
@ -232,6 +234,7 @@ public class QueryKey
if (metas[i].getDataCache() == null)
return null;
accessPathClassNames.add(metas[i].getDescribedType().getName());
subTimeout = metas[i].getDataCacheTimeout();
if (subTimeout != -1 && subTimeout < timeout)
timeout = subTimeout;
@ -466,4 +469,12 @@ public class QueryKey
_rangeEnd = in.readLong ();
_timeout = in.readInt ();
}
/**
* Returns the set of the accessPathClassnames that exists in the query
* @return -- Returns a set of accesspath classnames.
*/
public Set<String> getAcessPathClassNames() {
return this._accessPathClassNames;
}
}

View File

@ -31,6 +31,7 @@ public class QueryResult
private final long _ex;
private long _timestamp = 0L;
/**
* Constructor; supply corresponding query key and result data.
*/
@ -64,4 +65,20 @@ public class QueryResult
public boolean isTimedOut() {
return _ex != -1 && _ex < System.currentTimeMillis();
}
/**
* Sets the timestamp of the query result.
* @param ts -- Timestamp value in long
*/
public void setTimestamp(long ts) {
this._timestamp = ts;
}
/**
* Returns the timestamp of the query result.
* @return -- the timestamp value in long
*/
public long getTimestamp() {
return this._timestamp;
}
}

View File

@ -0,0 +1,81 @@
/*
* 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.persistence.jdbc.query.cache;
import java.util.ArrayList;
import java.util.Collection;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.OneToMany;
import javax.persistence.Version;
import org.apache.openjpa.persistence.DataCache;
@Entity
//@MappedSuperclass
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="PARTTYPE")
@DataCache
abstract public class Part {
@Id int partno;
@Column(length=20)
String name;
int inventory;
@OneToMany(mappedBy="child",cascade=CascadeType.PERSIST)
protected Collection<Usage> usedIn = new ArrayList<Usage>();
@Version
long version;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPartno() {
return partno;
}
public void setPartno(int partno) {
this.partno = partno;
}
public Collection<Usage> getUsedIn() {
return usedIn;
}
public void setUsedIn(Collection<Usage> usedIn) {
this.usedIn = usedIn;
}
public int getInventory() {
return inventory;
}
public void setInventory(int inventory) {
this.inventory = inventory;
}
}

View File

@ -0,0 +1,93 @@
/*
* 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.persistence.jdbc.query.cache;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import org.apache.openjpa.persistence.DataCache;
@Entity
@DataCache
public class PartBase extends Part {
double cost;
double mass;
int backOrder;
@ManyToMany(mappedBy="supplies")
protected List<Supplier> suppliers = new ArrayList<Supplier>();
public PartBase() {}
public PartBase(int partno, String name, double cost, double mass){
this.partno=partno;
this.name = name;
this.cost = cost;
this.mass= mass;
this.backOrder=0;
this.inventory=0;
}
public double getCost() {
return cost;
}
public void setCost(double cost) {
this.cost = cost;
}
public double getMass() {
return mass;
}
public void setMass(double mass) {
this.mass = mass;
}
public Collection<Supplier> getSuppliers() {
return suppliers;
}
public void setSuppliers(List<Supplier> suppliers) {
this.suppliers = suppliers;
}
public String toString() {
String sup= "";
if (getSuppliers()!=null)
for (Supplier s : getSuppliers()){
sup= sup+s.sid+",";
}
return "PartBase:"+partno+" name:+"+name+" cost:"+cost+" mass:"+mass+" supplies=["+sup+"]";
}
public int getBackOrder() {
return backOrder;
}
public void setBackOrder(int backOrder) {
this.backOrder = backOrder;
}
}

View File

@ -0,0 +1,96 @@
/*
* 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.persistence.jdbc.query.cache;
import java.util.ArrayList;
import java.util.Collection;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.OneToMany;
import org.apache.openjpa.persistence.DataCache;
@Entity
@DataCache
public class PartComposite extends Part {
double assemblyCost;
double assemblyTime;
double massIncrement;
@OneToMany( mappedBy="parent")
Collection<Usage> partsUsed = new ArrayList<Usage>();
public PartComposite() {}
public PartComposite(int partno, String name, double asmCost, double massInc) {
this.partno=partno;
this.name=name;
assemblyCost=asmCost;
massIncrement=massInc;
inventory=0;
}
public PartComposite addSubPart(EntityManager em, int quantity, Part subpart) {
Usage use = new Usage( this, quantity, subpart);
em.persist(use);
return this;
}
public double getAssemblyCost() {
return assemblyCost;
}
public void setAssemblyCost(double assemblyCost) {
this.assemblyCost = assemblyCost;
}
public double getMassIncrement() {
return massIncrement;
}
public void setMassIncrement(double massIncrement) {
this.massIncrement = massIncrement;
}
public String toString() {
return "PartComposite:"+partno+" name:+"+name+" assemblyCost:"+assemblyCost+" massIncrement:"+massIncrement;
}
public Collection<Usage> getPartsUsed() {
return partsUsed;
}
public void setPartsUsed(Collection<Usage> partsUsed) {
this.partsUsed = partsUsed;
}
public double getAssemblyTime() {
return assemblyTime;
}
public void setAssemblyTime(double assemblyTime) {
this.assemblyTime = assemblyTime;
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.persistence.jdbc.query.cache;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.persistence.*;
import org.apache.openjpa.persistence.DataCache;
@Entity
@DataCache
public class Supplier {
@Id int sid;
@Column(length=20)
String name;
@ManyToMany
List<PartBase> supplies = new ArrayList<PartBase>();
@Version
long version;
public Supplier(){}
public Supplier(int sid, String name){
this.sid=sid;
this.name=name;
}
public Supplier addPart( PartBase p ) {
supplies.add(p);
p.getSuppliers().add(this);
return this;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSid() {
return sid;
}
public void setSid(int sid) {
this.sid = sid;
}
public Collection<PartBase> getSupplies() {
return supplies;
}
public void setSupplies(List<PartBase> supplies) {
this.supplies = supplies;
}
public String toString() {
return "Supplier:"+sid+" name:+"+name;
}
}

View File

@ -0,0 +1,207 @@
/*
* 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.persistence.jdbc.query.cache;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.apache.openjpa.datacache.ConcurrentQueryCache;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import org.apache.openjpa.persistence.QueryResultCacheImpl;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;
import org.apache.openjpa.util.CacheMap;
public class TestQueryTimestampEviction extends SingleEMFTestCase {
public void setUp() throws Exception {
super.setUp(Part.class, PartBase.class, PartComposite.class,
Supplier.class, Usage.class,
"openjpa.DataCache", "true",
"openjpa.QueryCache",
"CacheSize=1000, evictPolicy='timestamp'",
"openjpa.RemoteCommitProvider", "sjvm");
if (recreateData) {
// deletes any data leftover data in the database due to the failed
// last run of this testcase
deleteAllData();
reCreateData();
}
}
private boolean deleteData = false;
private boolean recreateData = true;
public void testLoadQueries() {
loadQueryCache();
int cacheSizeBeforeUpdate = queryCacheGet();
updateAnEntity();
int cacheSizeAfterUpdate = queryCacheGet();
// If evictPolicy is timestamp the querycache size should be equal to
// cacheSizeBeforeUpdate value.
String evictPolicy = getQueryCache().getEvictPolicy();
if(evictPolicy.equalsIgnoreCase("timestamp"))
assertEquals(cacheSizeBeforeUpdate, cacheSizeAfterUpdate);
this.recreateData = false;
}
public void testEviction() {
loadQueryCache();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
updateAnEntity();
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
String insert1 = "insert into part(partno,parttype,name,cost,mass)" +
" values(13,'PartBase','breakes',1000.0,100.0)";
em.createNativeQuery(insert1).executeUpdate();
String insert2 = "insert into supplier_part(suppliers_sid," +
"supplies_partno) values(1,13)";
em.createNativeQuery(insert2).executeUpdate();
em.getTransaction().commit();
em.close();
em = emf.createEntityManager();
em.getTransaction().begin();
String sql = "select partno from part where cost > 120 ";
Query nativeq = em.createNativeQuery(sql);
List nativelist = nativeq.getResultList();
em.getTransaction().commit();
em.close();
em = emf.createEntityManager();
em.getTransaction().begin();
Query q = em.createQuery("select p from PartBase p where p.cost>?1");
q.setParameter(1, new Double(120));
List jpalist = q.getResultList();
em.getTransaction().commit();
em.close();
// The resultlist of nativelist and jpalist should be the same
// in both eviction policies(dafault/timestamp)
assertEquals(nativelist.size(),jpalist.size());
this.deleteData = true;
this.recreateData = true;
}
private void loadQueryCache() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
String qry = "select p from PartBase p where p.cost > ?1";
for (int i=120; i<155; i++) {
Query q = em.createQuery(qry);
q.setParameter(1, new Double(i));
q.getResultList();
}
em.getTransaction().commit();
em.close();
}
private void updateAnEntity() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//Update entity
PartBase p = em.find(PartBase.class,11);
double oldcost = p.getCost();
if (p != null)
p.setCost((oldcost + 10.0));
em.getTransaction().commit();
em.close();
}
private ConcurrentQueryCache getQueryCache() {
OpenJPAEntityManagerFactory oemf = OpenJPAPersistence.cast(emf);
QueryResultCacheImpl scache = (QueryResultCacheImpl) oemf.
getQueryResultCache();
return (ConcurrentQueryCache ) scache.getDelegate();
}
private int queryCacheGet() {
ConcurrentQueryCache dcache = getQueryCache();
CacheMap map = dcache.getCacheMap();
return map.size();
}
public void tearDown() throws Exception {
if (deleteData)
deleteAllData();
}
private void reCreateData() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Supplier s1 = new Supplier(1, "S1");
em.persist(s1);
Supplier s2 = new Supplier(2, "S2");
em.persist(s2);
Supplier s3 = new Supplier(3, "S3");
em.persist(s3);
PartBase p1 = new PartBase(10, "Wheel", 150, 15.00);
em.persist(p1);
PartBase p2 = new PartBase(11, "Frame", 550.00, 25.00);
em.persist(p2);
PartBase p3 = new PartBase(12, "HandleBar", 125.00, 80.00);
em.persist(p3);
s1.addPart(p1).addPart(p2).addPart(p3);
s2.addPart(p1).addPart(p3);
PartComposite p4 = new PartComposite(20, "Bike", 180, 1.0);
em.persist(p4);
p4.addSubPart(em, 2, p1).addSubPart(em, 1, p2).addSubPart(em, 1, p3);
em.getTransaction().commit();
em.close();
}
private void deleteAllData() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.createNativeQuery("delete from supplier_part").executeUpdate();
em.createQuery("delete from PartBase s").executeUpdate();
em.createQuery("delete from Supplier s").executeUpdate();
em.createQuery("delete from Usage u").executeUpdate();
em.createQuery("delete from Part p").executeUpdate();
em.getTransaction().commit();
em.close();
}
}

View File

@ -0,0 +1,95 @@
/*
* 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.persistence.jdbc.query.cache;
import javax.persistence.*;
import org.apache.openjpa.persistence.DataCache;
@Entity
@DataCache(timeout=100000)
public class Usage {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
int id;
int quantity;
@ManyToOne
Part child;
@ManyToOne
PartComposite parent ;
@Version
long version;
public Usage(PartComposite p, int quantity, Part subpart) {
parent=p;
this.quantity=quantity;
parent.getPartsUsed().add(this);
setChild(subpart);
subpart.getUsedIn().add(this);
}
// JPA entity needs a public no-arg constructor !
public Usage() {}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Part getParent() {
return parent;
}
public void setParent(PartComposite parent) {
this.parent = parent;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public Part getChild() {
return child;
}
public void setChild(Part child) {
this.child = child;
}
public String toString() {
return "Usage:"+id+" quantity:"+quantity+" child:"+child.getPartno()+" parent"+parent.getPartno();
}
}