001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020 021 package org.apache.geronimo.samples.daytrader; 022 023 import java.math.BigDecimal; 024 import java.sql.Timestamp; 025 import java.util.ArrayList; 026 import java.util.Collection; 027 028 import javax.ejb.CreateException; 029 import javax.ejb.EJBException; 030 import javax.ejb.FinderException; 031 import javax.ejb.SessionBean; 032 import javax.ejb.SessionContext; 033 import javax.jms.Connection; 034 import javax.jms.ConnectionFactory; 035 import javax.jms.MessageProducer; 036 import javax.jms.Queue; 037 import javax.jms.Session; 038 import javax.jms.TextMessage; 039 import javax.jms.Topic; 040 import javax.naming.InitialContext; 041 import javax.naming.NamingException; 042 import javax.persistence.EntityManager; 043 import javax.persistence.Query; 044 045 import org.apache.geronimo.samples.daytrader.ejb.Trade; 046 import org.apache.geronimo.samples.daytrader.util.FinancialUtils; 047 import org.apache.geronimo.samples.daytrader.util.Log; 048 import org.apache.geronimo.samples.daytrader.util.MDBStats; 049 050 public class TradeJPA implements SessionBean { 051 052 private EntityManager entityManager; 053 054 private SessionContext context = null; 055 056 private ConnectionFactory qConnFactory = null; 057 private Queue queue = null; 058 private ConnectionFactory tConnFactory = null; 059 private Topic streamerTopic = null; 060 061 private boolean publishQuotePriceChange = true; 062 063 private void queueOrderInternal(Integer orderID, boolean twoPhase) 064 throws javax.jms.JMSException { 065 if (Log.doTrace()) Log.trace("TradeBean:queueOrderInternal", orderID); 066 067 Connection conn = null; 068 Session sess = null; 069 try { 070 conn = qConnFactory.createConnection(); 071 sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE); 072 MessageProducer msgProducer = sess.createProducer(queue); 073 TextMessage message = sess.createTextMessage(); 074 075 message.setStringProperty("command", "neworder"); 076 message.setIntProperty("orderID", orderID); 077 message.setBooleanProperty("twoPhase", twoPhase); 078 message.setText("neworder: orderID=" + orderID + " runtimeMode=EJB twoPhase=" + twoPhase); 079 message.setLongProperty("publishTime", System.currentTimeMillis()); 080 081 if (Log.doTrace()) Log.trace("TradeBean:queueOrder Sending message: " + message.getText()); 082 msgProducer.send(message); 083 } 084 catch (javax.jms.JMSException e) { 085 throw e; // pass the exception back 086 } 087 088 finally { 089 if (conn != null) 090 conn.close(); 091 if (sess != null) 092 sess.close(); 093 } 094 } 095 096 /** 097 * @see org.apache.geronimo.samples.daytrader.TradeServices#queueOrder(Integer, boolean) 098 */ 099 100 public void queueOrder(Integer orderID, boolean twoPhase) 101 throws Exception { 102 if (Log.doTrace()) Log.trace("TradeBean:queueOrder", orderID, twoPhase); 103 if (twoPhase) 104 queueOrderInternal(orderID, true); 105 else { 106 // invoke the queueOrderOnePhase method -- which requires a new transaction 107 // the queueOrder will run in it's own transaction thus not requiring a 108 // 2-phase commit 109 ((Trade) context.getEJBObject()).queueOrderOnePhase(orderID); 110 } 111 } 112 113 114 /** 115 * @see Trade#queueOrderOnePhase(Integer) 116 * Queue the Order identified by orderID to be processed in a One Phase commit 117 * <p/> 118 * In short, this method is deployed as TXN REQUIRES NEW to avoid a 119 * 2-phase commit transaction across Entity and MDB access 120 */ 121 122 public void queueOrderOnePhase(Integer orderID) { 123 try { 124 if (Log.doTrace()) Log.trace("TradeBean:queueOrderOnePhase", orderID); 125 queueOrderInternal(orderID, false); 126 } catch (Exception e) { 127 throw new EJBException(e.getMessage(), e); 128 } 129 } 130 131 class quotePriceComparator implements java.util.Comparator { 132 public int compare(Object quote1, Object quote2) { 133 double change1 = ((QuoteDataBean) quote1).getChange(); 134 double change2 = ((QuoteDataBean) quote2).getChange(); 135 return new Double(change2).compareTo(change1); 136 } 137 } 138 139 public MarketSummaryDataBean getMarketSummary() 140 throws Exception { 141 142 143 MarketSummaryDataBean marketSummaryData; 144 try { 145 if (Log.doTrace()) { 146 Log.trace("TradeBean:getMarketSummary -- getting market summary"); 147 } 148 149 //Find Trade Stock Index Quotes (Top 100 quotes) 150 //ordered by their change in value 151 Collection quotes; 152 // if (orderBySQLSupported) { 153 Query query = entityManager.createNamedQuery("quotesByChange"); 154 quotes = query.getResultList(); 155 // } 156 // else 157 // quotes = quoteHome.findTSIAQuotes(); 158 159 //SORT by price change the collection of stocks if the AppServer 160 // does not support the "ORDER BY" SQL clause 161 // if (! orderBySQLSupported) { 162 //if (Log.doTrace()) 163 // Log.trace("TradeBean:getMarketSummary() -- Sorting TSIA quotes"); 164 // ArrayList sortedQuotes = new ArrayList(quotes); 165 // java.util.Collections.sort(sortedQuotes, new TradeJPA.quotePriceComparator()); 166 // quotes = sortedQuotes; 167 // } 168 //SORT END 169 QuoteDataBean[] quoteArray = (QuoteDataBean[]) quotes.toArray(new QuoteDataBean[quotes.size()]); 170 ArrayList<QuoteDataBean> topGainers = new ArrayList<QuoteDataBean>(5); 171 ArrayList<QuoteDataBean> topLosers = new ArrayList<QuoteDataBean>(5); 172 BigDecimal TSIA = FinancialUtils.ZERO; 173 BigDecimal openTSIA = FinancialUtils.ZERO; 174 double totalVolume = 0.0; 175 176 if (quoteArray.length > 5) { 177 for (int i = 0; i < 5; i++) 178 topGainers.add(quoteArray[i]); 179 for (int i = quoteArray.length - 1; i >= quoteArray.length - 5; i--) 180 topLosers.add(quoteArray[i]); 181 182 for (QuoteDataBean quote : quoteArray) { 183 BigDecimal price = quote.getPrice(); 184 BigDecimal open = quote.getOpen(); 185 double volume = quote.getVolume(); 186 TSIA = TSIA.add(price); 187 openTSIA = openTSIA.add(open); 188 totalVolume += volume; 189 } 190 TSIA = TSIA.divide(new BigDecimal(quoteArray.length), 191 FinancialUtils.ROUND); 192 openTSIA = openTSIA.divide(new BigDecimal(quoteArray.length), 193 FinancialUtils.ROUND); 194 } 195 /* This is an alternate approach using ejbSelect methods 196 * In this approach an ejbSelect is used to select only the 197 * current price and open price values for the TSIA 198 LocalQuote quote = quoteHome.findOne(); 199 BigDecimal TSIA = quote.getTSIA(); 200 openTSIA = quote.getOpenTSIA(); 201 Collection topGainers = quote.getTopGainers(5); 202 Collection topLosers = quote.getTopLosers(5); 203 LocalQuote quote = (LocalQuote)topGainers.iterator().next(); 204 double volume = quote.getTotalVolume(); 205 * 206 */ 207 208 marketSummaryData = new MarketSummaryDataBean(TSIA, openTSIA, totalVolume, topGainers, topLosers); 209 } 210 catch (Exception e) { 211 Log.error("TradeBean:getMarketSummary", e); 212 throw new EJBException("TradeBean:getMarketSummary -- error ", e); 213 } 214 return marketSummaryData; 215 } 216 217 public QuoteDataBean createQuote(String symbol, String companyName, BigDecimal price) { 218 try { 219 QuoteDataBean quote = new QuoteDataBean(symbol, companyName, 0, price, price, price, price, 0); 220 entityManager.persist(quote); 221 if (Log.doTrace()) Log.trace("TradeBean:createQuote-->" + quote); 222 return quote; 223 } catch (Exception e) { 224 Log.error("TradeBean:createQuote -- exception creating Quote", e); 225 throw new EJBException(e); 226 } 227 } 228 229 public QuoteDataBean getQuote(String symbol) { 230 231 if (Log.doTrace()) Log.trace("TradeBean:getQuote", symbol); 232 return entityManager.find(QuoteDataBean.class, symbol); 233 // } catch (NamingException e) { 234 // throw new EJBException(e); 235 //Cannot find quote for given symbol 236 // Log.error("TradeBean:getQuote--> Symbol: " + symbol + " cannot be found"); 237 // BigDecimal z = new BigDecimal(0.0); 238 // return new QuoteDataBean("Error: Symbol " + symbol + " not found", "", 0.0, z, z, z, z, 0.0); 239 // } 240 } 241 242 public Collection getAllQuotes() 243 throws Exception { 244 if (Log.doTrace()) Log.trace("TradeBean:getAllQuotes"); 245 246 Query query = entityManager.createNamedQuery("allQuotes"); 247 return query.getResultList(); 248 } 249 250 public QuoteDataBean updateQuotePriceVolume(String symbol, BigDecimal changeFactor, double sharesTraded) 251 throws Exception { 252 253 if (!TradeConfig.getUpdateQuotePrices()) 254 return new QuoteDataBean(); 255 256 if (Log.doTrace()) 257 Log.trace("TradeBean:updateQuote", symbol, changeFactor); 258 259 // try { 260 QuoteDataBean quote = entityManager.find(QuoteDataBean.class, symbol); 261 BigDecimal oldPrice = quote.getPrice(); 262 263 if (quote.getPrice().equals(TradeConfig.PENNY_STOCK_PRICE)) { 264 changeFactor = TradeConfig.PENNY_STOCK_RECOVERY_MIRACLE_MULTIPLIER; 265 } 266 267 BigDecimal newPrice = changeFactor.multiply(oldPrice).setScale(2, BigDecimal.ROUND_HALF_UP); 268 269 quote.setPrice(newPrice); 270 quote.setVolume(quote.getVolume() + sharesTraded); 271 // TODO find out if requires new here is really intended -- it is backwards, change can get published w/o it occurring. 272 ((Trade) context.getEJBObject()).publishQuotePriceChange(quote, oldPrice, changeFactor, sharesTraded); 273 // publishQuotePriceChange(quote, oldPrice, changeFactor, sharesTraded); 274 return quote; 275 // } catch (FinderException fe) { 276 // Cannot find quote for given symbol 277 // Log.error("TradeBean:updateQuotePriceVolume--> Symbol: " + symbol + " cannot be found"); 278 // quoteData = new QuoteDataBean("Error: Symbol " + symbol + " not found"); 279 // } 280 } 281 282 public void publishQuotePriceChange(QuoteDataBean quoteData, BigDecimal oldPrice, BigDecimal changeFactor, double sharesTraded) { 283 if (!publishQuotePriceChange) 284 return; 285 if (Log.doTrace()) 286 Log.trace("TradeBean:publishQuotePricePublishing -- quoteData = " + quoteData); 287 288 Connection conn = null; 289 Session sess = null; 290 291 try { 292 conn = tConnFactory.createConnection(); 293 sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE); 294 MessageProducer msgProducer = sess.createProducer(streamerTopic); 295 TextMessage message = sess.createTextMessage(); 296 297 String command = "updateQuote"; 298 message.setStringProperty("command", command); 299 message.setStringProperty("symbol", quoteData.getSymbol()); 300 message.setStringProperty("company", quoteData.getCompanyName()); 301 message.setStringProperty("price", quoteData.getPrice().toString()); 302 message.setStringProperty("oldPrice", oldPrice.toString()); 303 message.setStringProperty("open", quoteData.getOpen().toString()); 304 message.setStringProperty("low", quoteData.getLow().toString()); 305 message.setStringProperty("high", quoteData.getHigh().toString()); 306 message.setDoubleProperty("volume", quoteData.getVolume()); 307 308 message.setStringProperty("changeFactor", changeFactor.toString()); 309 message.setDoubleProperty("sharesTraded", sharesTraded); 310 message.setLongProperty("publishTime", System.currentTimeMillis()); 311 message.setText("Update Stock price for " + quoteData.getSymbol() + " old price = " + oldPrice + " new price = " + quoteData.getPrice()); 312 313 msgProducer.send(message); 314 } 315 catch (Exception e) { 316 throw new EJBException(e.getMessage(), e); // pass the exception back 317 } 318 finally { 319 try { 320 if (conn != null) 321 conn.close(); 322 if (sess != null) 323 sess.close(); 324 } catch (Exception e) { 325 throw new EJBException(e.getMessage(), e); // pass the exception back 326 } 327 } 328 } 329 330 public OrderDataBean buy(String userID, String symbol, double quantity, int orderProcessingMode) 331 throws Exception { 332 333 OrderDataBean order; 334 BigDecimal total; 335 try { 336 if (Log.doTrace()) 337 Log.trace("TradeBean:buy", userID, symbol, quantity, orderProcessingMode); 338 /* The following commented code shows alternative forms of the finder needed for this 339 * method 340 * The first alternative requires a 2-table join. Some database cannot allocate an Update 341 * Lock on a join select. 342 * 343 * The second alternative shows the finder being executed without allocation an update 344 * lock on the row. Normally, an update lock would not be necessary, but is required if 345 * the same user logs in multiple times to avoid a deadlock situation. 346 * 347 * The third alternative runs the finder and allocates an update lock on the row(s) 348 * 349 LocalAccount account = accountHome.findByUserIDForUpdate(userID); 350 351 LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate(); 352 */ 353 AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID); 354 AccountDataBean account = profile.getAccount(); 355 QuoteDataBean quote = entityManager.find(QuoteDataBean.class, symbol); 356 HoldingDataBean holding = null; //The holding will be created by this buy order 357 // Integer orderID = keySequence.getNextID("order"); 358 359 order = createOrder(account, quote, holding, "buy", quantity); 360 361 //UPDATE - account should be credited during completeOrder 362 BigDecimal price = quote.getPrice(); 363 BigDecimal orderFee = order.getOrderFee(); 364 BigDecimal balance = account.getBalance(); 365 total = (new BigDecimal(quantity).multiply(price)).add(orderFee); 366 account.setBalance(balance.subtract(total)); 367 368 if (orderProcessingMode == TradeConfig.SYNCH) 369 completeOrderInternal(order.getOrderID()); 370 else if (orderProcessingMode == TradeConfig.ASYNCH) 371 // Invoke the queueOrderOnePhase method w/ TXN requires new attribute 372 // to side-step a 2-phase commit across DB and JMS access 373 queueOrder(order.getOrderID(), false); 374 else //TradeConfig.ASYNC_2PHASE 375 queueOrder(order.getOrderID(), true); 376 } 377 catch (Exception e) { 378 Log.error("TradeBean:buy(" + userID + "," + symbol + "," + quantity + ") --> failed", e); 379 /* On exception - cancel the order */ 380 //TODO figure out how to do this with JPA 381 // if (order != null) order.cancel(); 382 throw new EJBException(e); 383 } 384 return order; 385 } 386 387 public OrderDataBean sell(String userID, Integer holdingID, int orderProcessingMode) 388 throws Exception { 389 390 OrderDataBean order; 391 BigDecimal total; 392 try { 393 if (Log.doTrace()) 394 Log.trace("TradeBean:sell", userID, holdingID, orderProcessingMode); 395 396 /* Some databases cannot allocate an update lock on a JOIN 397 * use the second approach below to acquire update lock 398 LocalAccount account = accountHome.findByUserIDForUpdate(userID); 399 */ 400 401 AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID); 402 AccountDataBean account = profile.getAccount(); 403 HoldingDataBean holding = entityManager.find(HoldingDataBean.class, holdingID); 404 // catch (ObjectNotFoundException oe) { 405 // Log.error("TradeBean:sell User " + userID + " attempted to sell holding " + holdingID + " which has already been sold"); 406 // OrderDataBean orderData = new OrderDataBean(); 407 // orderData.setOrderStatus("cancelled"); 408 // return orderData; 409 // } 410 QuoteDataBean quote = holding.getQuote(); 411 double quantity = holding.getQuantity(); 412 order = createOrder(account, quote, holding, "sell", quantity); 413 414 //UPDATE the holding purchase data to signify this holding is "inflight" to be sold 415 // -- could add a new holdingStatus attribute to holdingEJB 416 holding.setPurchaseDate(new java.sql.Timestamp(0)); 417 418 //UPDATE - account should be credited during completeOrder 419 BigDecimal price = quote.getPrice(); 420 BigDecimal orderFee = order.getOrderFee(); 421 BigDecimal balance = account.getBalance(); 422 total = (new BigDecimal(quantity).multiply(price)).subtract(orderFee); 423 account.setBalance(balance.add(total)); 424 425 if (orderProcessingMode == TradeConfig.SYNCH) 426 completeOrderInternal(order.getOrderID()); 427 else if (orderProcessingMode == TradeConfig.ASYNCH) 428 queueOrder(order.getOrderID(), false); 429 else //TradeConfig.ASYNC_2PHASE 430 queueOrder(order.getOrderID(), true); 431 432 } 433 catch (Exception e) { 434 Log.error("TradeBean:sell(" + userID + "," + holdingID + ") --> failed", e); 435 //TODO figure out JPA cancel 436 // if (order != null) order.cancel(); 437 //UPDATE - handle all exceptions like: 438 throw new EJBException("TradeBean:sell(" + userID + "," + holdingID + ")", e); 439 } 440 return order; 441 } 442 443 444 public Collection getOrders(String userID) 445 throws FinderException, Exception { 446 447 if (Log.doTrace()) 448 Log.trace("TradeBean:getOrders", userID); 449 450 /* The following commented code shows alternative forms of the finder needed for this 451 * method 452 * The first alternative requires a 2-table join. Some database cannot allocate an Update 453 * Lock on a join select. 454 * 455 * The second alternative shows the finder being executed without allocation an update 456 * lock on the row. Normally, an update lock would not be necessary, but is required if 457 * the same user logs in multiple times to avoid a deadlock situation. 458 * 459 * The third alternative runs the finder and allocates an update lock on the row(s) 460 * 461 462 Collection orders = accountHome.findByUserID(userID).getOrders(); 463 464 LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate(); 465 */ 466 AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID); 467 AccountDataBean account = profile.getAccount(); 468 return account.getOrders(); 469 470 // ArrayList dataBeans = new ArrayList(); 471 // if (orders == null) return dataBeans; 472 // 473 // Iterator it = orders.iterator(); 474 //TODO: return top 5 orders for now -- next version will add a getAllOrders method 475 // also need to get orders sorted by order id descending 476 // int i = 0; 477 // while ((it.hasNext()) && (i++ < 5)) 478 // dataBeans.add(((OrderData) it.next()).getDataBean()); 479 // 480 // return dataBeans; 481 } 482 483 public Collection getClosedOrders(String userID) 484 throws FinderException, Exception { 485 if (Log.doTrace()) 486 Log.trace("TradeBean:getClosedOrders", userID); 487 488 try { 489 490 /* The following commented code shows alternative forms of the finder needed for this 491 * method 492 * The first alternative requires a 2-table join. Some database cannot allocate an Update 493 * Lock on a join select. 494 * 495 * The second alternative shows the finder being executed without allocation an update 496 * lock on the row. Normally, an update lock would not be necessary, but is required if 497 * the same user logs in multiple times to avoid a deadlock situation. 498 * 499 * The third alternative runs the finder and allocates an update lock on the row(s) 500 * 501 Collection orders = orderHome.findClosedOrdersForUpdate(userID); 502 * 503 LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccount(); 504 */ 505 506 //Get the primary keys for all the closed Orders for this account. 507 Query query = entityManager.createNamedQuery("closedOrders"); 508 query.setParameter("userID", userID); 509 Collection results = query.getResultList(); 510 Query updateStatus = entityManager.createNamedQuery("completeClosedOrders"); 511 updateStatus.setParameter("userID", userID); 512 updateStatus.executeUpdate(); 513 return results; 514 // if (ordersKeys == null) return dataBeans; 515 // 516 // Iterator it = ordersKeys.iterator(); 517 // while (it.hasNext()) { 518 // Integer orderKey = (Integer) it.next(); 519 // LocalOrder order = (LocalOrder) orderHome.findByPrimaryKeyForUpdate(orderKey); 520 //Complete the order 521 // order.setOrderStatus("completed"); 522 // dataBeans.add(order.getDataBean()); 523 // } 524 525 } 526 catch (Exception e) { 527 Log.error("TradeBean.getClosedOrders", e); 528 throw new EJBException("TradeBean.getClosedOrders - error", e); 529 } 530 } 531 532 533 public OrderDataBean completeOrder(Integer orderID, boolean twoPhase) 534 throws Exception { 535 if (Log.doTrace()) Log.trace("TradeBean:completeOrder", orderID + " twoPhase=" + twoPhase); 536 if (twoPhase) 537 return completeOrderInternal(orderID); 538 else { 539 // invoke the completeOrderOnePhase -- which requires a new transaction 540 // the completeOrder will run in it's own transaction thus not requiring a 541 // 2-phase commit 542 return ((Trade) context.getEJBObject()).completeOrderOnePhase(orderID); 543 } 544 } 545 546 //completeOrderOnePhase method is deployed w/ TXN_REQUIRES_NEW 547 //thus the completeOrder call from the MDB should not require a 2-phase commit 548 public OrderDataBean completeOrderOnePhase(Integer orderID) { 549 try { 550 if (Log.doTrace()) Log.trace("TradeBean:completeOrderOnePhase", orderID); 551 return completeOrderInternal(orderID); 552 } catch (Exception e) { 553 throw new EJBException(e.getMessage(), e); 554 } 555 } 556 557 private OrderDataBean completeOrderInternal(Integer orderID) 558 throws Exception { 559 560 OrderDataBean order = entityManager.find(OrderDataBean.class, orderID); 561 562 if (order == null) { 563 Log.error("TradeBean:completeOrderInternal -- Unable to find Order " + orderID + " FBPK returned " + order); 564 return null; 565 } 566 567 if (order.isCompleted()) 568 throw new EJBException("Error: attempt to complete Order that is already completed\n" + order); 569 570 AccountDataBean account = order.getAccount(); 571 QuoteDataBean quote = order.getQuote(); 572 HoldingDataBean holding = order.getHolding(); 573 BigDecimal price = order.getPrice(); 574 double quantity = order.getQuantity(); 575 576 /* 577 * getProfile is marked as Pess. Update to get a DB lock 578 * Here we invoke getProfileForRead which is deployed to not 579 * lock the DB (Pess. Read) 580 */ 581 String userID = account.getProfile().getUserID(); 582 583 /* 584 * total = (quantity * purchasePrice) + orderFee 585 */ 586 587 588 if (Log.doTrace()) Log.trace( 589 "TradeBeanInternal:completeOrder--> Completing Order " + order.getOrderID() 590 + "\n\t Order info: " + order 591 + "\n\t Account info: " + account 592 + "\n\t Quote info: " + quote 593 + "\n\t Holding info: " + holding); 594 595 596 if (order.isBuy()) { 597 /* Complete a Buy operation 598 * - create a new Holding for the Account 599 * - deduct the Order cost from the Account balance 600 */ 601 602 HoldingDataBean newHolding = createHolding(account, quote, quantity, price); 603 order.setHolding(newHolding); 604 } 605 606 if (order.isSell()) { 607 /* Complete a Sell operation 608 * - remove the Holding from the Account 609 * - deposit the Order proceeds to the Account balance 610 */ 611 if (holding == null) { 612 Log.error("TradeBean:completeOrderInternal -- Unable to sell order " + order.getOrderID() + " holding already sold"); 613 order.cancel(); 614 return order; 615 } else { 616 entityManager.remove(holding); 617 order.setHolding(null); 618 // holding.remove(); 619 // holding = null; 620 } 621 622 // This is managed by the container 623 // order.setHolding(null); 624 625 } 626 order.setOrderStatus("closed"); 627 628 order.setCompletionDate(new java.sql.Timestamp(System.currentTimeMillis())); 629 630 if (Log.doTrace()) Log.trace( 631 "TradeBean:completeOrder--> Completed Order " + order.getOrderID() 632 + "\n\t Order info: " + order 633 + "\n\t Account info: " + account 634 + "\n\t Quote info: " + quote 635 + "\n\t Holding info: " + holding); 636 637 if (Log.doTrace()) 638 Log.trace("Calling TradeAction:orderCompleted from Session EJB using Session Object"); 639 //FUTURE All getEJBObjects could be local -- need to add local I/F 640 641 TradeServices trade = (TradeServices) context.getEJBObject(); 642 TradeAction tradeAction = new TradeAction(trade); 643 644 //signify this order for user userID is complete 645 //TODO figure out wtf is going on, all implementations of this method throw NotSupportedException !!! 646 //Also try to figure out the business reason this should be in a separate tx ?!?!?!? 647 //tradeAction.orderCompleted(userID, orderID); 648 return order; 649 } 650 651 652 //These methods are used to provide the 1-phase commit runtime option for TradeDirect 653 // Basically these methods are deployed as txn requires new and invoke TradeDirect methods 654 // There is no mechanism outside of EJB to start a new transaction 655 public OrderDataBean completeOrderOnePhaseDirect(Integer orderID) { 656 try { 657 if (Log.doTrace()) 658 Log.trace("TradeBean:completeOrderOnePhaseDirect -- completing order by calling TradeDirect orderID=" + orderID); 659 return (new org.apache.geronimo.samples.daytrader.direct.TradeDirect()).completeOrderOnePhase(orderID); 660 } catch (Exception e) { 661 throw new EJBException(e.getMessage(), e); 662 } 663 } 664 665 public void cancelOrderOnePhaseDirect(Integer orderID) { 666 try { 667 if (Log.doTrace()) 668 Log.trace("TradeBean:cancelOrderOnePhaseDirect -- cancelling order by calling TradeDirect orderID=" + orderID); 669 (new org.apache.geronimo.samples.daytrader.direct.TradeDirect()).cancelOrderOnePhase(orderID); 670 } catch (Exception e) { 671 throw new EJBException(e.getMessage(), e); 672 } 673 } 674 675 676 public void cancelOrder(Integer orderID, boolean twoPhase) 677 throws Exception { 678 if (Log.doTrace()) Log.trace("TradeBean:cancelOrder", orderID + " twoPhase=" + twoPhase); 679 if (twoPhase) 680 cancelOrderInternal(orderID); 681 else { 682 // invoke the cancelOrderOnePhase -- which requires a new transaction 683 // the completeOrder will run in it's own transaction thus not requiring a 684 // 2-phase commit 685 ((Trade) context.getEJBObject()).cancelOrderOnePhase(orderID); 686 } 687 } 688 689 //cancelOrderOnePhase method is deployed w/ TXN_REQUIRES_NEW 690 //thus the completeOrder call from the MDB should not require a 2-phase commit 691 public void cancelOrderOnePhase(Integer orderID) { 692 try { 693 if (Log.doTrace()) Log.trace("TradeBean:cancelOrderOnePhase", orderID); 694 cancelOrderInternal(orderID); 695 } catch (Exception e) { 696 throw new EJBException(e.getMessage(), e); 697 } 698 } 699 700 701 private void cancelOrderInternal(Integer orderID) 702 throws Exception { 703 OrderDataBean order = entityManager.find(OrderDataBean.class, orderID); 704 order.cancel(); 705 } 706 707 708 public void orderCompleted(String userID, Integer orderID) 709 throws Exception { 710 throw new UnsupportedOperationException("TradeBean:orderCompleted method not supported"); 711 } 712 713 public HoldingDataBean createHolding( 714 AccountDataBean account, 715 QuoteDataBean quote, 716 double quantity, 717 BigDecimal purchasePrice) 718 throws Exception { 719 HoldingDataBean newHolding = new HoldingDataBean(quantity, purchasePrice, new Timestamp(System.currentTimeMillis()), account, quote); 720 entityManager.persist(newHolding); 721 return newHolding; 722 } 723 724 public Collection getHoldings(String userID) 725 throws FinderException, Exception { 726 if (Log.doTrace()) 727 Log.trace("TradeBean:getHoldings", userID); 728 Query query = entityManager.createNamedQuery("holdingsByUserID"); 729 query.setParameter("userID", userID); 730 return query.getResultList(); 731 } 732 733 public HoldingDataBean getHolding(Integer holdingID) 734 throws FinderException, Exception { 735 if (Log.doTrace()) 736 Log.trace("TradeBean:getHolding", holdingID); 737 return entityManager.find(HoldingDataBean.class, holdingID); 738 } 739 740 741 742 public OrderDataBean createOrder( 743 AccountDataBean account, 744 QuoteDataBean quote, 745 HoldingDataBean holding, 746 String orderType, 747 double quantity) 748 throws CreateException, Exception { 749 750 OrderDataBean order; 751 752 if (Log.doTrace()) 753 Log.trace( 754 "TradeBean:createOrder(orderID=" 755 + " account=" 756 + ((account == null) ? null : account.getAccountID()) 757 + " quote=" 758 + ((quote == null) ? null : quote.getSymbol()) 759 + " orderType=" 760 + orderType 761 + " quantity=" 762 + quantity); 763 try { 764 assert quote != null; 765 order = new OrderDataBean(orderType, "open", new Timestamp(System.currentTimeMillis()), null, quantity, quote.getPrice().setScale(FinancialUtils.SCALE, FinancialUtils.ROUND), TradeConfig.getOrderFee(orderType), account, quote, holding); 766 entityManager.persist(order); 767 } 768 catch (Exception e) { 769 Log.error("TradeBean:createOrder -- failed to create Order", e); 770 throw new EJBException("TradeBean:createOrder -- failed to create Order", e); 771 } 772 return order; 773 } 774 775 public AccountDataBean login(String userID, String password) 776 throws FinderException, Exception { 777 778 /* The following commented code shows alternative forms of the finder needed for this 779 * method 780 * The first alternative requires a 2-table join. Some database cannot allocate an Update 781 * Lock on a join select. 782 * 783 * The second alternative shows the finder being executed without allocation an update 784 * lock on the row. Normally, an update lock would not be necessary, but is required if 785 * the same user logs in multiple times to avoid a deadlock situation. 786 * 787 * The third alternative runs the finder and allocates an update lock on the row(s) 788 * 789 LocalAccount account = accountHome.findByUserIDForUpdate(userID); 790 791 LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate(); 792 */ 793 794 AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID); 795 if (profile == null) { 796 throw new EJBException("No such user: " + userID); 797 } 798 AccountDataBean account = profile.getAccount(); 799 800 if (Log.doTrace()) 801 Log.trace("TradeBean:login", userID, password); 802 account.login(password); 803 if (Log.doTrace()) 804 Log.trace("TradeBean:login(" + userID + "," + password + ") success" + account); 805 return account; 806 } 807 808 public void logout(String userID) 809 throws FinderException, Exception { 810 if (Log.doTrace()) 811 Log.trace("TradeBean:logout", userID); 812 813 /* The following commented code shows alternative forms of the finder needed for this 814 * method 815 * The first alternative requires a 2-table join. Some database cannot allocate an Update 816 * Lock on a join select. 817 * 818 * The second alternative shows the finder being executed without allocation an update 819 * lock on the row. Normally, an update lock would not be necessary, but is required if 820 * the same user logs in multiple times to avoid a deadlock situation. 821 * 822 * The third alternative runs the finder and allocates an update lock on the row(s) 823 824 * LocalAccount account = accountHome.findByUserIDForUpdate(userID); 825 LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate(); 826 * 827 */ 828 AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID); 829 AccountDataBean account = profile.getAccount(); 830 831 832 if (Log.doTrace()) Log.trace("TradeBean:logout(" + userID + ") success"); 833 account.logout(); 834 } 835 836 public AccountDataBean register( 837 String userID, 838 String password, 839 String fullname, 840 String address, 841 String email, 842 String creditcard, 843 BigDecimal openBalance) 844 throws CreateException, Exception { 845 846 AccountDataBean account = new AccountDataBean(0, 0, null, new Timestamp(System.currentTimeMillis()), openBalance, openBalance, null); 847 AccountProfileDataBean profile = new AccountProfileDataBean(userID, password, fullname, address, email, creditcard); 848 account.setProfile(profile); 849 //are both of these necessary? 850 profile.setAccount(account); 851 //this should save the linked profile as well? 852 entityManager.persist(account); 853 //apparently doesn't?? 854 entityManager.persist(profile); 855 return account; 856 } 857 858 public AccountDataBean getAccountData(String userID) { 859 860 if (Log.doTrace()) 861 Log.trace("TradeBean:getAccountData", userID); 862 863 /* The following commented code shows alternative forms of the finder needed for this 864 * method 865 * The first alternative requires a 2-table join. Some database cannot allocate an Update 866 * Lock on a join select. 867 * 868 * The second alternative shows the finder being executed without allocation an update 869 * lock on the row. Normally, an update lock would not be necessary, but is required if 870 * the same user logs in multiple times to avoid a deadlock situation. 871 * 872 * The third alternative runs the finder and allocates an update lock on the row(s) 873 * 874 LocalAccount account = accountHome.findByUserID(userID); 875 876 LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate(); 877 */ 878 AccountProfileDataBean profile = entityManager.find(AccountProfileDataBean.class, userID); 879 return profile.getAccount(); 880 } 881 882 public AccountProfileDataBean getAccountProfileData(String userID) 883 throws FinderException, Exception { 884 if (Log.doTrace()) 885 Log.trace("TradeBean:getAccountProfileData", userID); 886 887 /* The following commented code shows alternative forms of the finder needed for this 888 * method 889 * The first alternative requires a 2-table join. Some database cannot allocate an Update 890 * Lock on a join select. 891 * 892 * The second alternative shows the finder being executed without allocation an update 893 * lock on the row. Normally, an update lock would not be necessary, but is required if 894 * the same user logs in multiple times to avoid a deadlock situation. 895 * 896 * The third alternative runs the finder and allocates an update lock on the row(s) 897 * 898 LocalAccount account = accountHome.findByUserID(userID); 899 900 LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKey(userID)).getAccountForUpdate(); 901 */ 902 return entityManager.find(AccountProfileDataBean.class, userID); 903 } 904 905 public AccountProfileDataBean updateAccountProfile(AccountProfileDataBean accountProfileData) 906 throws FinderException, Exception { 907 908 if (Log.doTrace()) 909 Log.trace("TradeBean:updateAccountProfileData", accountProfileData); 910 911 //TODO this might not be correct 912 entityManager.merge(accountProfileData); 913 // LocalAccount account = ((LocalAccountProfile) profileHome.findByPrimaryKeyForUpdate(accountProfileData.getUserID())).getAccountForUpdate(); 914 // 915 // accountProfileData = 916 // account.updateAccountProfile(accountProfileData); 917 return accountProfileData; 918 } 919 920 public RunStatsDataBean resetTrade(boolean deleteAll) 921 throws Exception { 922 if (Log.doTrace()) 923 Log.trace("TradeBean:resetTrade", deleteAll); 924 925 //Clear MDB Statistics 926 MDBStats.getInstance().reset(); 927 // Reset Trade 928 return new org.apache.geronimo.samples.daytrader.direct.TradeDirect().resetTrade(deleteAll); 929 } 930 931 /** 932 * provides a simple session method with no database access to test performance of a simple 933 * path through a stateless session 934 * 935 * @param investment amount 936 * @param NetValue current value 937 * @return return on investment as a percentage 938 */ 939 public double investmentReturn(double investment, double NetValue) { 940 if (Log.doTrace()) 941 Log.trace("TradeBean:investmentReturn"); 942 943 double diff = NetValue - investment; 944 return diff / investment; 945 946 } 947 948 /** 949 * This method provides a ping test for a 2-phase commit operation 950 * 951 * @param symbol to lookup 952 * @return quoteData after sending JMS message 953 */ 954 public QuoteDataBean pingTwoPhase(String symbol) { 955 try { 956 if (Log.doTrace()) Log.trace("TradeBean:pingTwoPhase", symbol); 957 Connection conn = null; 958 Session sess = null; 959 try { 960 961 //Get a Quote and send a JMS message in a 2-phase commit 962 QuoteDataBean quote = entityManager.find(QuoteDataBean.class, symbol); 963 964 conn = qConnFactory.createConnection(); 965 sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE); 966 MessageProducer msgProducer = sess.createProducer(queue); 967 TextMessage message = sess.createTextMessage(); 968 969 String command = "ping"; 970 message.setStringProperty("command", command); 971 message.setLongProperty("publishTime", System.currentTimeMillis()); 972 message.setText("Ping message for queue java:comp/env/jms/TradeBrokerQueue sent from TradeSessionEJB:pingTwoPhase at " + new java.util.Date()); 973 974 msgProducer.send(message); 975 return quote; 976 } finally { 977 if (conn != null) 978 conn.close(); 979 if (sess != null) 980 sess.close(); 981 } 982 983 } catch (Exception e) { 984 throw new EJBException(e.getMessage(), e); 985 } 986 } 987 988 /* Required javax.ejb.SessionBean interface methods */ 989 990 public TradeJPA() { 991 } 992 993 private static boolean warnJMSFailure = true; 994 995 public void ejbCreate() throws CreateException { 996 try { 997 998 if (Log.doTrace()) 999 Log.trace("TradeBean:ejbCreate -- JNDI lookups of EJB and JMS resources"); 1000 1001 InitialContext ic = new InitialContext(); 1002 1003 publishQuotePriceChange = (Boolean) ic.lookup("java:comp/env/publishQuotePriceChange"); 1004 boolean updateQuotePrices = (Boolean) ic.lookup("java:comp/env/updateQuotePrices"); 1005 TradeConfig.setUpdateQuotePrices(updateQuotePrices); 1006 1007 try { 1008 qConnFactory = (ConnectionFactory) ic.lookup("java:comp/env/jms/QueueConnectionFactory"); 1009 tConnFactory = (ConnectionFactory) ic.lookup("java:comp/env/jms/TopicConnectionFactory"); 1010 streamerTopic = (Topic) ic.lookup("java:comp/env/jms/TradeStreamerTopic"); 1011 queue = (Queue) ic.lookup("java:comp/env/jms/TradeBrokerQueue"); 1012 } 1013 catch (Exception e) { 1014 if (TradeJPA.warnJMSFailure) { 1015 TradeJPA.warnJMSFailure = false; 1016 Log.error("TradeBean:ejbCreate Unable to lookup JMS Resources\n\t -- Asynchronous mode will not work correctly and Quote Price change publishing will be disabled", e); 1017 } 1018 publishQuotePriceChange = false; 1019 } 1020 1021 } catch (Exception e) { 1022 Log.error("TradeBean:ejbCreate: Lookup of Local Entity Homes Failed\n" + e); 1023 e.printStackTrace(); 1024 //UPDATE 1025 //throw new CreateException(e.toString()); 1026 } 1027 1028 } 1029 1030 public void ejbRemove() { 1031 try { 1032 if (Log.doTrace()) 1033 Log.trace("TradeBean:ejbRemove"); 1034 } 1035 catch (Exception e) { 1036 Log.error(e, "Unable to close Queue or Topic connection on Session EJB remove"); 1037 } 1038 } 1039 1040 public void ejbActivate() { 1041 } 1042 1043 public void ejbPassivate() { 1044 } 1045 1046 public void setSessionContext(SessionContext sc) { 1047 context = sc; 1048 if (sc != null) { 1049 try { 1050 entityManager = (EntityManager) new InitialContext().lookup("java:comp/env/jpa/daytrader"); 1051 } catch (NamingException e) { 1052 throw new EJBException("could not get Naming Context", e); 1053 } 1054 } else { 1055 entityManager = null; 1056 } 1057 1058 } 1059 }