As noted above, the key-fields element contains a key-field for each primary key field of the current entity. The key-field element uses the same syntax as the cmp-field element of the entity, except that key-field does not support the not-null option. Key fields of a relation-table are automatically not null, because they are the primary key of the table. On the other hand, foreign key fields must be nullable by default. This is because the CMP specification requires an insert into the database after the ejbCreate method and an update to it after to pick up CMR changes made in ejbPostCreate. Since the EJB specification does not allow a relationship to be modified until ejbPostCreate, a foreign key will be initially set to null. There is a similar problem with removal. You can change this insert behavior using the jboss.xml insert-after-ejb-post-create container configuration flag. The following example illustrates the creation of a new bean configuration that uses insert-after-ejb-post-create by default.
<jboss>
<!-- ... -->
<container-configurations>
<container-configuration extends="Standard CMP 2.x EntityBean">
<container-name>INSERT after ejbPostCreate Container</container-name>
<insert-after-ejb-post-create>true</insert-after-ejb-post-create>
</container-configuration>
</container-configurations>
</jboss>
An alternate means of working around the non-null foreign key issue is to map the foreign key elements onto non-null CMP fields. In this case you simply populate the foreign key fields in ejbCreate using the associated CMP field setters.
i wanted to post a more clear explanation.
I was able to configure jboss to delay the insert after ejbpost create.
In order to do this, the following lines must be added to jboss.xml:
<container-configurations>
<container-configuration extends="Standard CMP 2.x EntityBean">
<container-name>INSERTPOSTCREATE</container-name>
<insert-after-ejb-post-create>true</insert-after-ejb-post-create>
</container-configuration>
</container-configurations>
Another thing that must be done, is to mark the entity beans to use this configuration (this is also done in jboss.xml)
The last thing that must be done, when the primary key is generated by the database server, is to set a dummy id for the entity bean in ejbCreate, something like: setId(new Integer(DUMMY_ID)); this will be overwritten by the database server with the right id. This hack must we done, otherwise an CreateException will be thrown (this is probably a jboss bug for now - not taking in account the fact that the primary key will be generated by the database server).
<jboss>
<enterprise-beans>
<entity>
<ejb-name>ProductKeyEJB</ejb-name>
<configuration-name>INSERT after ejbPostCreate Container</configuration-name>
<local-jndi-name>ProductKeyHomeLocal</local-jndi-name>
</entity>
</enterprise-beans>
<container-configurations>
<container-configuration extends="Standard CMP 2.x EntityBean">
<container-name>INSERT after ejbPostCreate Container</container-name>
<insert-after-ejb-post-create>true</insert-after-ejb-post-create>
</container-configuration>
</container-configurations>
</jboss>
Product Bean
-------------
package com.example.ejb.product;
import javax.ejb.*;
import java.util.*;
public abstract class ProductBean implements EntityBean {
public Integer ejbCreate(String name) throws CreateException {
//setId(id);
setName(name);
return null;
}
public void ejbPostCreate(String name) {
}
public abstract Set getProductKeys();
public abstract void setProductKeys( Set productKeys );
public abstract Integer getId();
public abstract void setId(Integer id);
public abstract String getName();
public abstract void setName(String name);
public void setEntityContext(EntityContext ctx) {
}
public void unsetEntityContext() {
}
public void ejbLoad() {
}
public void ejbStore() {
}
public void ejbActivate() {
}
public void ejbPassivate() {
}
public void ejbRemove() {
}
}
package com.example.ejb.product;
import javax.ejb.*;
import java.util.*;
public interface ProductHomeLocal extends EJBLocalHome {
public ProductLocal create(String name) throws CreateException,
EJBException;
public ProductLocal findByPrimaryKey(Integer id) throws FinderException;
public ProductLocal findByName( String name ) throws FinderException;
}
package com.example.ejb.product;
import javax.ejb.*;
import java.util.*;
public interface ProductLocal extends EJBLocalObject {
public String getName() throws EJBException;
public void setName(String name) throws EJBException;
public Integer getId() throws EJBException;
public void setId(Integer id) throws EJBException;
public Set getProductKeys() throws EJBException;
public void setProductKeys( Set productKeys ) throws EJBException;
}
package com.example.ejb.productkey;
import java.util.Iterator;
import java.util.Set;
import javax.ejb.CreateException;
import javax.ejb.EntityBean;
import javax.ejb.EntityContext;
import javax.ejb.FinderException;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import com.example.ejb.product.ProductHomeLocal;
import com.example.ejb.product.ProductLocal;
public abstract class ProductKeyBean implements EntityBean{
public Integer ejbCreate(String key,String productName) throws CreateException{
setKey(key);
setId(new Integer("11"));
//setProduct(productId);
return null;
}
public void ejbPostCreate(String key,String productName) throws FinderException{
InitialContext jndiContext;
try {
jndiContext = new InitialContext();
Object obj = jndiContext.lookup("ProductHomeLocal");
ProductHomeLocal productHome = (ProductHomeLocal) obj;
ProductLocal product = productHome.findByName(productName);
Set set = product.getProductKeys();
Iterator it = set.iterator();
while(it.hasNext()){
System.out.println(" KEYS ::: "+it.next());
}
setProduct(product);
} catch (NamingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public abstract String getKey();
public abstract void setKey(String key);
public abstract Integer getId();
public abstract void setId(Integer id);
public abstract ProductLocal getProduct();
public abstract void setProduct(ProductLocal productId);
public void ejbActivate() {
// TODO Auto-generated method stub
}
public void ejbLoad() {
// TODO Auto-generated method stub
}
public void ejbPassivate() {
// TODO Auto-generated method stub
}
public void ejbRemove() {
// TODO Auto-generated method stub
}
public void ejbStore() {
// TODO Auto-generated method stub
}
public void setEntityContext(EntityContext arg0) {
// TODO Auto-generated method stub
}
public void unsetEntityContext() {
// TODO Auto-generated method stub
}
}
package com.example.ejb.productkey;
import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.EJBLocalHome;
import javax.ejb.FinderException;
public interface ProductKeyHomeLocal extends EJBLocalHome{
public ProductKeyLocal create(String key,String productName) throws CreateException, EJBException;
public ProductKeyLocal findByPrimaryKey(Integer id) throws FinderException, EJBException;
}
package com.example.ejb.productkey;
import javax.ejb.EJBException;
import javax.ejb.EJBLocalObject;
import com.example.ejb.product.ProductLocal;
public interface ProductKeyLocal extends EJBLocalObject {
public String getKey();
public void setKey(String key);
public ProductLocal getProduct() throws EJBException;
public void setProduct( ProductLocal product ) throws EJBException;
}
package com.example.ejb.productmanager;
import java.rmi.RemoteException;
import java.util.Set;
import javax.ejb.SessionContext;
import javax.naming.InitialContext;
import com.example.ejb.product.ProductHomeLocal;
import com.example.ejb.product.ProductLocal;
import com.example.ejb.productkey.ProductKeyHomeLocal;
import com.example.ejb.productkey.ProductKeyLocal;
public class ProductManagerBean implements javax.ejb.SessionBean {
public SessionContext context;
private InitialContext ic = null;
public void ejbCreate() {
}
public void ejbActivate() {
}
public void ejbPassivate() {
}
public void ejbRemove() {
}
public void setSessionContext(SessionContext ctx) {
context = ctx;
}
public String addProduct(String productName, String key) throws RemoteException {
String str = "";
try {
// Get our Product Home
InitialContext jndiContext = getInitialContext();
/*Object obj = jndiContext.lookup("ProductHomeLocal");
ProductHomeLocal productHome = (ProductHomeLocal) obj;*/
// Create our product
/*ProductLocal product = productHome.create(productName);
System.out.println(" create product obj :::"+product.getId());
System.out.println(" create product obj :::"+product.getName());
System.out.println("prim ::"+product.getPrimaryKey());*/
// Instead of creating a new product, find the existing product to add a key to it
/*ProductLocal product = productHome.findByName(productName);
System.out.println("prim ::"+product.getPrimaryKey());
System.out.println("get Id ::"+product.getId());*/
// Get out Product Key Home
Object obj1 = jndiContext.lookup("ProductKeyHomeLocal");
ProductKeyHomeLocal productKeyHome = (ProductKeyHomeLocal) obj1;
// Create our product key
ProductKeyLocal productKeyLocal = productKeyHome.create(key,productName);
//productKeyLocal.setProduct(product);
//Set productKeys = product.getProductKeys();
//productKeys.add( productKeyLocal );
} catch (Exception e) {
e.printStackTrace();
}
return str;
}
public void removeProduct(Integer id) throws RemoteException {
try {
// Get our Product Home
InitialContext jndiContext = getInitialContext();
Object obj = jndiContext.lookup("ProductHomeLocal");
ProductHomeLocal productHome = (ProductHomeLocal) obj;
// Find our product
ProductLocal product = productHome.findByPrimaryKey(id);
product.remove();
} catch (Exception e) {
e.printStackTrace();
}
}
public void deleteProductByName(String productName) throws RemoteException{
try{
// Get our Product Home
InitialContext jndiContext = getInitialContext();
Object obj = jndiContext.lookup("ProductHomeLocal");
ProductHomeLocal productHome = (ProductHomeLocal) obj;
ProductLocal product = productHome.findByName(productName);
System.out.println("Before deleting checking the id::"+product.getId());
product.remove();
}catch(Exception e){
e.printStackTrace();
}
}
public void updateProduct(String productName) throws RemoteException{
try{
// Get our Product Home
InitialContext jndiContext = getInitialContext();
Object obj = jndiContext.lookup("ProductHomeLocal");
ProductHomeLocal productHome = (ProductHomeLocal) obj;
ProductLocal product = productHome.findByName(productName);
System.out.println("Before updating checking the id::"+product.getId());
product.setName("ModifiedProduct");
//product.remove();
}catch(Exception e){
e.printStackTrace();
}
}
public InitialContext getInitialContext()
throws javax.naming.NamingException {
if (this.ic == null) {
ic = new InitialContext();
}
return ic;
}
}
package com.example.ejb.productmanager;
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import java.rmi.RemoteException;
import javax.ejb.CreateException;
public interface ProductManagerHomeRemote extends javax.ejb.EJBHome {
public ProductManagerRemote create() throws RemoteException,
CreateException;
}
package com.example.ejb.productmanager;
import java.rmi.RemoteException;
public interface ProductManagerRemote extends javax.ejb.EJBObject {
public String addProduct(String productName,String key)
throws RemoteException;
public void removeProduct(Integer id) throws RemoteException;
public void deleteProductByName(String productName) throws RemoteException;
public void updateProduct(String productName) throws RemoteException;
}
package com.example.web.delegate;
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.resource.ResourceException;
import org.apache.struts.action.ActionForm;
import com.example.ejb.productmanager.ProductManagerHomeRemote;
import com.example.ejb.productmanager.ProductManagerRemote;
import com.example.web.form.ProductForm;
import com.example.web.locator.ServiceLocator;
import com.example.web.locator.ServiceLocatorException;
public class ProductDelegate {
private ProductManagerRemote productManager = null;
private static final Class homeClazz = com.example.ejb.productmanager.ProductManagerHomeRemote.class;
public ProductDelegate() throws ResourceException {
try {
ProductManagerHomeRemote home = (ProductManagerHomeRemote) ServiceLocator
.getInstance().getRemoteHome("ProductManagerHomeRemote",
homeClazz);
productManager = home.create();
} catch (ServiceLocatorException sle) {
// Translate Service Locator exception into application exception
throw new ResourceException(sle);
} catch (RemoteException e) {
e.printStackTrace();
} catch (CreateException e) {
e.printStackTrace();
}
}
public void addProduct(ActionForm form) throws ResourceException{
ProductForm prodForm = (ProductForm)form;
try{
System.out.println("Addin product thru delegate..........");
productManager.addProduct(prodForm.getProductName(), prodForm.getProductKey());
}catch(RemoteException e){
e.printStackTrace();
}
}
}
package com.example.web.locator;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.ejb.EJBHome;
import javax.ejb.EJBLocalHome;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
public class ServiceLocator {
private InitialContext initialContext;
private Map cache;
private static ServiceLocator serviceLocatorInstance;
static{
try{
serviceLocatorInstance = new ServiceLocator();
}catch(Exception sle){
sle.printStackTrace();
}
}
private ServiceLocator() throws ServiceLocatorException{
try{
initialContext = new InitialContext();
cache = Collections.synchronizedMap(new HashMap());
}catch(NamingException ne){
throw new ServiceLocatorException(ne);
}catch(Exception e){
throw new ServiceLocatorException(e);
}
}
public static ServiceLocator getInstance(){
return serviceLocatorInstance;
}
/*
* look up a local home given the JNDI name for the local home
*/
public EJBLocalHome getLocalHome(String jndiHomeName) throws ServiceLocatorException{
EJBLocalHome localHome = null;
try{
if(cache.containsKey(jndiHomeName)){
localHome = (EJBLocalHome) cache.get(jndiHomeName);
}else{
localHome = (EJBLocalHome) initialContext.lookup(jndiHomeName);
cache.put(jndiHomeName, localHome);
}
}catch(NamingException ne){
throw new ServiceLocatorException(ne);
}catch(Exception e){
throw new ServiceLocatorException(e);
}
return localHome;
}
/*
* lookup a remote home given the JNDI name for the remote home
*/
public EJBHome getRemoteHome(String jndiRemoteHomeName,Class remoteHomeClassName) throws ServiceLocatorException{
EJBHome remoteHome = null;
try{
if(cache.containsKey(jndiRemoteHomeName)){
remoteHome = (EJBHome) cache.get(jndiRemoteHomeName);
}else{
Object objref = (EJBHome) initialContext.lookup(jndiRemoteHomeName);
Object obj = PortableRemoteObject.narrow(objref, remoteHomeClassName);
if(obj instanceof EJBHome){
remoteHome = (EJBHome) obj;
cache.put(jndiRemoteHomeName, remoteHome);
}else{
throw new ServiceLocatorException("Not a instance of EJBHome");
}
}
}catch(NamingException ne){
throw new ServiceLocatorException(ne);
}catch(Exception e){
throw new ServiceLocatorException(e);
}
return remoteHome;
}
}
package com.example.web.locator;
public class ServiceLocatorException extends Exception{
String errorMsg = "";
public ServiceLocatorException(){
super();
errorMsg = "ServiceLocatorException";
}
public ServiceLocatorException(String error){
super(error);
errorMsg = error;
}
public ServiceLocatorException(Exception e){
super(e.getMessage());
errorMsg = e.getMessage();
}
public String getError(){
return errorMsg;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
<!-- EJB References -->
<ejb-ref>
<ejb-ref-name>ejb/ProductManagerHomeRemote</ejb-ref-name>
<jndi-name>ejb/ProductManagerHomeRemote</jndi-name>
</ejb-ref>
</jboss-web>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>debug</param-name>
<param-value>3</param-value>
</init-param>
<init-param>
<param-name>detail</param-name>
<param-value>3</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<ejb-ref>
<ejb-ref-name>ejb/ProductManagerHomeRemote</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<home>com.example.ejb.productmanager.ProductManagerHomeRemote</home>
<remote>com.example.ejb.productmanager.ProductManagerRemote</remote>
<ejb-link>ProductManagerHomeRemote</ejb-link>
</ejb-ref>
</web-app>
<?xml version="1.0"?>
<jboss>
<enterprise-beans>
<entity>
<ejb-name>ProductEJB</ejb-name>
<local-jndi-name>ProductHomeLocal</local-jndi-name>
</entity>
<entity>
<ejb-name>ProductKeyEJB</ejb-name>
<configuration-name>INSERT after ejbPostCreate Container</configuration-name>
<local-jndi-name>ProductKeyHomeLocal</local-jndi-name>
</entity>
<session>
<ejb-name>ProductManagerEJB</ejb-name>
<jndi-name>ProductManagerHomeRemote</jndi-name>
</session>
</enterprise-beans>
<container-configurations>
<container-configuration extends="Standard CMP 2.x EntityBean">
<container-name>INSERT after ejbPostCreate Container</container-name>
<insert-after-ejb-post-create>true</insert-after-ejb-post-create>
</container-configuration>
</container-configurations>
</jboss>
<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise
JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd">
<ejb-jar>
<enterprise-beans>
<entity>
<ejb-name>ProductEJB</ejb-name>
<local-home>com.example.ejb.product.ProductHomeLocal</local-home>
<local>com.example.ejb.product.ProductLocal</local>
<ejb-class>com.example.ejb.product.ProductBean</ejb-class>
<persistence-type>Container</persistence-type>
<prim-key-class>java.lang.Integer</prim-key-class>
<reentrant>False</reentrant>
<cmp-version>2.x</cmp-version>
<abstract-schema-name>Product</abstract-schema-name>
<cmp-field><field-name>id</field-name></cmp-field>
<cmp-field><field-name>name</field-name></cmp-field>
<primkey-field>id</primkey-field>
<security-identity><use-caller-identity /></security-identity>
<query>
<query-method>
<method-name>findByName</method-name>
<method-params>
<method-param>java.lang.String</method-param>
</method-params>
</query-method>
<ejb-ql>
SELECT OBJECT(p) FROM Product p WHERE p.name = ?1
</ejb-ql>
</query>
</entity>
<entity>
<ejb-name>ProductKeyEJB</ejb-name>
<local-home>com.example.ejb.productkey.ProductKeyHomeLocal</local-home>
<local>com.example.ejb.productkey.ProductKeyLocal</local>
<ejb-class>com.example.ejb.productkey.ProductKeyBean</ejb-class>
<persistence-type>Container</persistence-type>
<prim-key-class>java.lang.Integer</prim-key-class>
<reentrant>False</reentrant>
<cmp-version>2.x</cmp-version>
<abstract-schema-name>ProductKey</abstract-schema-name>
<cmp-field><field-name>id</field-name></cmp-field>
<cmp-field><field-name>key</field-name></cmp-field>
<primkey-field>id</primkey-field>
<security-identity><use-caller-identity /></security-identity>
</entity>
<session>
<ejb-name>ProductManagerEJB</ejb-name>
<home>com.example.ejb.productmanager.ProductManagerHomeRemote</home>
<remote>com.example.ejb.productmanager.ProductManagerRemote</remote>
<ejb-class>com.example.ejb.productmanager.ProductManagerBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
<ejb-local-ref>
<ejb-ref-name>ProductHomeLocal</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<local-home>com.example.ejb.product.ProductHomeLocal</local-home>
<local> com.example.ejb.product.ProductLocal</local>
<!-- ejb-link is required by jboss for local-refs. -->
<ejb-link>ProductEJB</ejb-link>
</ejb-local-ref>
<ejb-local-ref>
<ejb-ref-name>ProductKeyHomeLocal</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<local-home>com.example.ejb.productkey.ProductKeyHomeLocal</local-home>
<local> com.example.ejb.product.ProductKeyLocal</local>
<!-- ejb-link is required by jboss for local-refs. -->
<ejb-link>ProductKeyEJB</ejb-link>
</ejb-local-ref>
</session>
</enterprise-beans>
<relationships>
<ejb-relation>
<ejb-relation-name>Product-ProductKey</ejb-relation-name>
<ejb-relationship-role>
<ejb-relationship-role-name>Product-has-many-ProductKeys</ejb-relationship-role-name>
<multiplicity>One</multiplicity>
<relationship-role-source>
<ejb-name>ProductEJB</ejb-name>
</relationship-role-source>
<cmr-field>
<cmr-field-name>productKeys</cmr-field-name>
<cmr-field-type>java.util.Set</cmr-field-type>
</cmr-field>
</ejb-relationship-role>
<ejb-relationship-role>
<ejb-relationship-role-name>ProductKey-belongs-to-Product</ejb-relationship-role-name>
<multiplicity>Many</multiplicity>
<cascade-delete/>
<relationship-role-source>
<ejb-name>ProductKeyEJB</ejb-name>
</relationship-role-source>
<cmr-field>
<cmr-field-name>product</cmr-field-name>
</cmr-field>
</ejb-relationship-role>
</ejb-relation>
</relationships>
<assembly-descriptor>
<container-transaction>
<method>
<ejb-name>ProductEJB</ejb-name>
<method-name>*</method-name>
</method>
<method>
<ejb-name>ProductKeyEJB</ejb-name>
<method-name>*</method-name>
</method>
<method>
<ejb-name>ProductManagerEJB</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jbosscmp-jdbc PUBLIC
"-//JBoss//DTD JBOSSCMP-JDBC 4.0//EN"
"http://www.jboss.org/j2ee/dtd/jbosscmp-jdbc_4_0.dtd">
<jbosscmp-jdbc>
<defaults>
<datasource>java:/MySqlDS</datasource>
<datasource-mapping>mySQL</datasource-mapping>
<create-table>false</create-table>
<remove-table>false</remove-table>
</defaults>
<enterprise-beans>
<entity>
<ejb-name>ProductEJB</ejb-name>
<pk-constraint>false</pk-constraint>
<table-name>Product</table-name>
<cmp-field>
<field-name>name</field-name>
<column-name>product_name</column-name>
</cmp-field>
<!-- unknown-pk>
<unknown-pk-class>java.lang.Integer</unknown-pk-class>
<field-name>id</field-name>
<column-name>product_id</column-name>
<jdbc-type>INTEGER</jdbc-type>
<sql-type>INTEGER</sql-type>
<auto-increment/>
</unknown-pk-->
<cmp-field>
<field-name>id</field-name>
<column-name>product_id</column-name>
<auto-increment />
</cmp-field>
<entity-command name="mysql-get-generated-keys" />
</entity>
<entity>
<ejb-name>ProductKeyEJB</ejb-name>
<table-name>ProductKey</table-name>
<cmp-field>
<field-name>id</field-name>
<column-name>pk_id</column-name>
<auto-increment />
</cmp-field>
<cmp-field>
<field-name>key</field-name>
<column-name>pk_key</column-name>
</cmp-field>
<entity-command name="mysql-get-generated-keys" />
</entity>
</enterprise-beans>
<relationships>
<ejb-relation>
<ejb-relation-name>Product-ProductKey</ejb-relation-name>
<foreign-key-mapping />
<ejb-relationship-role>
<ejb-relationship-role-name>
Product-has-many-ProductKeys
</ejb-relationship-role-name>
<key-fields>
<key-field>
<field-name>id</field-name>
<column-name>product_id</column-name>
</key-field>
</key-fields>
</ejb-relationship-role>
<ejb-relationship-role>
<ejb-relationship-role-name>
ProductKey-belongs-to-Product
</ejb-relationship-role-name>
<key-fields />
</ejb-relationship-role>
</ejb-relation>
</relationships>
</jbosscmp-jdbc>
It sure was nice of your sister to lend us her car. Let's show our appreciation by sharing this tiny ad:
Smokeless wood heat with a rocket mass heater
https://woodheat.net
|