stSoftware
12 Beaconsfield St | Newport, New South Wales 2106 | 1300 78 73 78

stSoftware blog


Java Code to validate email addresses
30 Jul 2013

com/aspc/remote/util/net/EmailUtil.java public final class EmailUtil { /** * http://www.regular-expressions.info/email.html */ @RegEx private static final Pattern EMAIL_PATTERN = Pattern.compile( "^[A-Z0-9._%+\\-#']+@[A-Z0-9.-]+\\.(?:[A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|asia|jobs|museum)$", Pattern.CASE_INSENSITIVE); /** * Validate an email address. * * @param email the email to validate. * @param hostCache OPTIONAL cache of host names * @throws InvalidDataException details of the issue found. */ public static void validate(final String email, final Map hostCache) throws InvalidDataException { String invalidStrings[] = { "..", ".@" }; for (String illegal : invalidStrings) { if (email.contains(illegal)) { throw new InvalidDataException("email " + email + " may not contain '" + illegal + "'"); } } Matcher m = EMAIL_PATTERN.matcher(email.trim()); if (m.matches() == false) { throw new InvalidDataException(email + " invalid email"); } String[] split = email.split("@"); if (split.length != 2) { throw new InvalidDataException("no @ symbol"); } String tmpHost = split[1]; String message = checkMX(tmpHost, null); if (message != null) { throw new InvalidDataException(message); } } /** * check the MX record * * @param hostName host to check * @param hostCache OPTIONAL cache of host names * @return message if NOT valid otherwise NULL */ public static String checkMX(final String hostName, final Map hostCache) { String message = null; if (hostCache != null) { message = hostCache.get(hostName); } if (message == null) { @SuppressWarnings("UseOfObsoleteCollectionType") Hashtable env = new Hashtable(); env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); try { DirContext ictx = new InitialDirContext(env); Attributes attrs = ictx.getAttributes(hostName, new String[] { "MX" }); Attribute attr = attrs.get("MX"); if (attr == null) { message = "no MX record for " + hostName; } else if (attr.size() > 0) { message = ""; } else { message = "no MX record for " + hostName; } } catch (NamingException e) { message = e.getMessage(); } if (hostCache != null) { hostCache.put(hostName, message); } } if (message == null || message.isEmpty()) { return null; } return message; } }

Read More

Block hack attempts from all foreign & unknown locations.
25 Jul 2013

The file /etc/hosts.deny on Unix/Linux can block login attempts based on counties. The below is a simple host.deny file to block all countries but your own ( in my case Australia), sure this is not the whole answer to securing a system but it sure cuts down the number of hack attempts. sudo vi /etc/hosts.allow # # hosts.deny This file describes the names of the hosts which are # *not* allowed to use the local INET services, as decided # by the '/usr/sbin/tcpd' server. # # We will block *all* the...

Read More

Does renaming a page's path automatically redirect from the old page path?
19 Jul 2013

Pages can have any number of aliases. When a page's path is renamed the system will automatically add an alias from the page's previous path to the current page. This prevents 404 errors which are a well known cause of user frustration and lower SEO rankings. When a user requests a URL first a matching page is searched then the page aliases are searched, if a matching page alias is found then that page is used. There are NO 301/302 errors returned there is just many paths to the same page.

Read More

Can different sites be created for different host names?
19 Jul 2013

To make a site the default for a domain or set of domain names a comma separated list of host name patterns is entered. The default site is calculated to be the first PUBLISHED site with a matching host name pattern, if no matching sites then the first site will a blank host name list. The domain name registry itself must be done as normal to point to the system with providers such as goDaddy or Netregistry

Read More

Support for Questionnaires
18 Jul 2013

A sample questionnaire :- http://polls.stsoftware.com.au/site/climate/ To create a questionnaire:- List of questions:- Create a new question:- List of choices:- Resulting sample survey:-

Read More

Handlebar (mustache) support in stSoftware's CMS
2 Jun 2013

A quick demonstration of the Handlebar (mustache) support in stSoftware's CMS. https://github.com/jknack/handlebars.java

Read More

Page META description
13 May 2013

The HTML META tag "description" allows you to influence how your web pages are described and displayed in search results. A good description acts as a potential organic advertisement and encourages the viewer to click through to your site. A page META description should be short. The description in the tag should to be between 25 and 160 characters in length. A tag which is too long will be displayed truncated and can be seen as spam by search engines. The optimal...

Read More

WYSIWYG Content Editing that provides varying levels of WYSIWYG editing control depending upon the complexity of editing requirements?
10 May 2013

When in CMS design mode, when the designer mouses over a component an EDIT button is shown for 5 seconds. A orange dotted line will outline the component that will be edited. Editing a breadcrumb component:- Editing a text component:- Editing a HTML component:-

Read More

Using GWT super dev mode
15 Apr 2013

NOTE: You MUST be using GWT version 2.5.1 or better. The source maps were broken in GWT 2.5 Add to your GWT module :- com/aspc/cms/gwt/site/Site.gwt.xml Add to the build.properties file:- gwt.main.package=com.aspc.cms.gwt.site.Site  Start the superdev server:- Add the property SUPER_DEV_MODULE to the webserver startup In Chrome enable source map Select the Java source from the chrome "sources tab":- Now you'll be able to step through the Java source code in chrome

Read More

Reliably handling externally processed records in an highly concurrent environment.
12 Apr 2013

To reliably process records in a concurrent environment which interface with a external system (not included in the database transaction) we must adopt a "three step approach". The three steps are:- Take ownership of the record by marking as "processing" and prevent other processes from doing the same by using an optimistic locking strategy. Process the record itself in the external system (payment gateway, send email etc) Record the result of the processing in the database by marking the record... * Handling race conditions when processing record. * * Firstly this process need to take ownership of the record using * optimistic locking strategy to prevent other processes grabbing the * same record. * http://en.wikipedia.org/wiki/Optimistic_concurrency_control * * Process the record ( email in this case) * * Mark the record as completed. */ MutableDataSource mds = getConnection().getMutableDataSource(); // find the email to be sent email = (DBEmailSend) mds.findKey(gk); mds.markSavePoint("SEND_START");// Mark the start point. for (int i = 0; true; i++) { try { /* * record the current transaction so that if a concurrent process trys to change the email then a dirty cache will be thrown. */ email.forceLockedTransaction(); /** * check and change the status of the email. Change the status * to a intermediate status and save. * * The save process on this record is atomic and only one * process will be able to successfully change the status to * "PROCESSING". * */ String sendStatus = email.getString(DBEmailSend.DBFIELD_SEND_STATUS); if (sendStatus.equals(DBEmailSendStatus.LIST_ENTERED) == false && sendStatus.equals(DBEmailSendStatus.LIST_QUEUED) == false) { throw new Exception("Send status must be ENTERED or QUEUED"); } email.setValue(DBEmailSend.DBFIELD_SEND_STATUS, DBEmailSendStatus.LIST_PROCESSING); mds.save("Email send started processing:" + email); } catch (DirtyCacheException dce) { /** * A concurrent change has been detected. Rollback to the start * point and retry. If the other process has successfully taken * ownership of the record we will skip. */ mds.rollbackTo("SEND_START"); if (i == 9) { String errorMsg = "Cannot send email: " + s + " as repeatedly failed in dirty cache "; LOGGER.error(errorMsg); throw new Exception(errorMsg); } else { long msecs = (long) (1000.0 * Math.random()); Thread.sleep(msecs); continue; } } /** * The only way that we would successfully get to this step is that * we have taken ownership of the record by setting the status to * "PROCESSING". * * The actual send process will set the status to "OK" or "FAILED". * * If the machine is turned off or crashes at any point the only * email records that we need to check the actual status of is the * ones that are with the status "PROCESSING" */ email.send(mds); break; }

Read More

Multi-Threads: SyncBlock a replacement of the synchronized keyword
12 Apr 2013

When you call the method take() on a SyncBlock object you'll block up until the specified maximum number of seconds and then an error will be thrown if you are unable to obtain the lock on this object. You must ALWAYS call release() on any sync SyncBlock that you have obtained the lock on. The SyncBlock differs from the keyword synchronized in that it is interruptible and that it will timeout if it blocks for too long. The SyncBlock enhances a normal java.lang.concurrent.Lock in that it will interrupt... 6687 * notify all the class listeners 6688 * @param type the type of change MODIFY,DELETE or CREATE 6689 * @param gk the global key to notify of. 6690 */ 6691 private void notifyDBClassListeners( final String type, final GlobalKey gk) 6692 { 6693 String key = gk.getClassId().toString(); 6694 6695 /** 6696 * DEADLOCK found when this was a synchronized block. 6697 * now we are using a SyncBlock lock object which will timeout after 2 minutes if not successful. 6698 * 6699 * Once you have taken the lock the next statement must be the start of the try block so we never leave this 6700 * section without releasing the lock. 6701 */ 6702 dbClassListenersLock.take(); 6703 try 6704 { 6705 ArrayList list = (ArrayList)dbClassListeners.get( key); 6706 6707 if( list != null) 6708 { 6709 for( int i = 0; i < list.size(); i++) 6710 { 6711 DBClassListener listener; 6712 6713 listener = (DBClassListener)list.get( i); 6714 6715 if( type.equals( DBData.NOTIFY_MODIFIED)) 6716 { 6717 listener.eventObjectModified( gk, this); 6718 } 6719 else if( type.equals( DBData.NOTIFY_DELETED)) 6720 { 6721 listener.eventObjectDeleted( gk, this); 6722 } 6723 else if( type.equals( DBData.NOTIFY_CREATED)) 6724 { 6725 listener.eventObjectCreated( gk, this); 6726 } 6727 else 6728 { 6729 LOGGER.error( "Wrong type:" + type); 6730 } 6731 } 6732 } 6733 } 6734 catch( Throwable t) 6735 { 6736 LOGGER.warn( "ignored exception in listener", t); // Sr, 12/05/2005 Bug #5224 6737 } 6738 finally 6739 { 6740 /** 6741 * Always release the lock if obtained. 6742 */ 6743 dbClassListenersLock.release(); 6744 } 6745 } com/aspc/remote/util/misc/SyncBlock.java 107 /** 108 * release the lock 109 */ 110 public void release() 111 { 112 syncLock.unlock(); 113 } 114 115 /** 116 * take the lock and throw an error if you can't get it. 117 */ 118 public void take() 119 { 120 try 121 { 122 SyncLock tempLock = syncLock; 123 if (syncLock.tryLock(blockSeconds, TimeUnit.SECONDS) == false) 124 { 125 Thread ownerThread = syncLock.getOwner(); 126 127 if( ownerThread.isAlive() == false) 128 { 129 synchronized( this) 130 { 131 if( tempLock == syncLock) 132 { 133 syncLock = new SyncLock(); 134 } 135 } 136 137 LOGGER.fatal(this + " never released by " + ownerThread); 138 take(); 139 return; 140 } 141 142 StringBuilder sb = new StringBuilder( toString()); 143 sb.append("\n"); 144 Thread currentThread = Thread.currentThread(); 145 146 sb.append("Failed to get lock for thread: " + currentThread + "\n"); 147 148 for (StackTraceElement ste : currentThread.getStackTrace()) 149 { 150 sb.append("\t" + ste + "\n"); 151 } 152 153 if( ownerThread != null) 154 { 155 sb.append("\nLock held by thread: " + ownerThread + "\n"); 156 157 for (StackTraceElement ste : ownerThread.getStackTrace()) 158 { 159 sb.append("\t" + ste + "\n"); 160 } 161 sb.append("Interrupting holding thread"); 162 ownerThread.interrupt(); 163 } 164 else 165 { 166 sb.append("NO OWNER THREAD found"); 167 } 168 169 LOGGER.fatal(sb.toString()); 170 171 throw new DataBaseError("could not get the lock on: " + name); 172 } 173 } 174 catch (InterruptedException ex) 175 { 176 Thread.interrupted(); 177 LOGGER.warn( "could not take lock on " + name, ex); 178 Thread.currentThread().interrupt(); 179 throw new DataBaseError("could not get the lock on: " + name, ex); 180 } 181 } 182 183 class SyncLock extends ReentrantLock 184 { 185 public SyncLock( ) 186 { 187 super( true); 188 } 189 /** 190 * get the owner thread 191 * @return the owner thread 192 */ 193 @Override 194 public Thread getOwner()//NOPMD 195 { 196 return super.getOwner(); 197 } 198 } com/aspc/remote/util/misc/selftest/TestSyncBlock.java 88 89 /** 90 * check we recover from a lock that is never released. 91 */ 92 public void testNeverReleased() throws InterruptedException 93 { 94 final SyncBlock block = new SyncBlock( "never release", 2); 95 96 Runnable r = new Runnable( ) 97 { 98 public void run() 99 { 100 block.take(); 101 } 102 }; 103 Thread t = new Thread( r); 104 t.start(); 105 106 t.join( 120000); 107 108 block.take(); 109 } 110 111 /** 112 * check that we actually do block 113 */ 114 @SuppressWarnings("empty-statement") 115 public void testBlock() throws InterruptedException 116 { 117 final SyncBlock block = new SyncBlock( "long time", 10); 118 119 Runnable r = new Runnable( ) 120 { 121 public void run() 122 { 123 block.take(); 124 try 125 { 126 Thread.sleep(120000); 127 } 128 catch (InterruptedException ex) 129 { 130 LOGGER.warn("interrupted"); 131 } 132 finally 133 { 134 block.release(); 135 } 136 } 137 }; 138 Thread t = new Thread( r); 139 t.start(); 140 141 t.join( 1000); 142 143 try 144 { 145 block.take(); 146 fail( "should not succeed"); 147 } 148 catch( Throwable tw) 149 { 150 ;// this is good 151 } 152 t.interrupt(); 153 154 t.join( 5000); 155 block.take(); 156 } 157 158 159 /** 160 * check that deadlocks are handled 161 * @throws Exception a test failure 162 */ 163 public void testDeadlockHandled() throws Exception 164 { 165 a=new A(); 166 b=new B(); 167 Thread at = new Thread( a); 168 169 at.start(); 170 Thread bt = new Thread( b); 171 172 bt.start(); 173 174 long start = System.currentTimeMillis(); 175 while( start + 120000 > System.currentTimeMillis()) 176 { 177 if( a.calling && b.calling ) break; 178 Thread.sleep(100); 179 } 180 181 synchronized( marker) 182 { 183 marker.notifyAll(); 184 } 185 LOGGER.info("waiting for detection"); 186 at.join(240000); 187 bt.join(240000); 188 189 if( a.theException == null && b.theException == null) 190 { 191 fail( "The threads were not interrupted"); 192 } 193 194 assertFalse( "should have finished", at.isAlive()); 195 assertFalse( "should have finished", bt.isAlive()); 196 } 197 198 class A implements Runnable 199 { 200 private final SyncBlock block = new SyncBlock("A block", 2); 201 boolean calling; 202 Throwable theException; 203 204 public void run() 205 { 206 try 207 { 208 callB(); 209 } 210 catch( Throwable e) 211 { 212 theException = e; 213 LOGGER.warn( "got cancelled", e); 214 } 215 } 216 217 public void hello() 218 { 219 block.take(); 220 try 221 { 222 LOGGER.info("hello A"); 223 } 224 finally 225 { 226 block.release(); 227 } 228 } 229 230 private void callB() throws InterruptedException 231 { 232 block.take(); 233 try 234 { 235 calling=true; 236 synchronized( marker) 237 { 238 marker.wait(120000); 239 } 240 LOGGER.info("call B"); 241 b.hello(); 242 } 243 finally 244 { 245 block.release(); 246 } 247 } 248 } 249 250 class B implements Runnable 251 { 252 boolean calling; 253 Throwable theException; 254 private final SyncBlock block = new SyncBlock("A block", 2); 255 256 public void run() 257 { 258 try 259 { 260 callA(); 261 } 262 catch( Throwable e) 263 { 264 theException = e; 265 LOGGER.warn( "got cancelled", e); 266 } 267 } 268 269 public void hello() 270 { 271 block.take(); 272 try 273 { 274 LOGGER.info("hello B"); 275 } 276 finally 277 { 278 block.release(); 279 } 280 } 281 282 private void callA() throws InterruptedException 283 { 284 block.take(); 285 try 286 { 287 calling=true; 288 synchronized( marker) 289 { 290 marker.wait(120000); 291 } 292 LOGGER.info("call A"); 293 a.hello(); 294 } 295 finally 296 { 297 block.release(); 298 } 299 } 300 } 301 302 private A a; 303 private B b;

Read More

Multi-Threads: secondary cache design
12 Apr 2013

1 /* 2 * Copyright (c) 2013 ASP Converters pty ltd 3 * 4 * www.aspconverters.com.au. 5 * 6 * All Rights Reserved. 7 * 8 * This software is the proprietary information of 9 * ASP Converters Pty Ltd. 10 * Use is subject to license terms. 11 */ 12 import com.aspc.DBObj.*; 13 import com.aspc.DBObj.Listeners.*; 14 15 /** 16 * Use a secondary cache to fetch a "Thing". We are in a multi-machine / multi-processor / 17 * multi-user / multi-threaded environment. Records can and do change at any time from one 18 * line to the next. A few points to look out for:- 19 * 20 * 1) All work must be done with local variables so that we don"t get Null 21 * Pointer Exceptions when the cache is cleared while we are in this method. 22 * 23 * 2) Threads can take their copies of object variables which are only flushed/sync"d when 24 * synchronized is called on the object. 25 * 26 * 3) Database queries etc. can take a while to run (specially if the query returns multiple rows) 27 * a record in the result set maybe changed and a message sent/clear cache called before the 28 * result is returned. This case must be handled ( it happens a lot with bulk records) 29 * 30 * 4) Synchronizing the method synchronizes the whole Object. So for complex objects like Company 31 * or DBClass which may have many secondary caches we would be blocking a fetch of something 32 * that is in memory due to a fetch of something else that is not. 34 * 5) Having complex logic within the synchronized block which calls other objects with 35 * synchronized blocks it is easy to cause Java deadlocks. A deadlock within Java will NEVER 36 * return unlike a normal database deadlock. 37 */ 38 public class Bits extends DBObject implements DependanceListener, ReloadEventListener 39 { 40 /** 41 * Std. DBObject constructor. 42 * 43 * @param def The class of this object 44 * @param dataSource The datasource for this object 45 * @throws Exception A serious problem occurred 46 */ 47 public Bits(DBClass def, DataSource dataSource) throws Exception 48 { 49 super( def, dataSource); 50 } 51 52 /** 53 * Sample secondary cache. 54 * 55 * Step 1. 56 * Enter a synchronized block so that we see a clean version of cache of "Thing" and the cache 57 * of thing is prevented while we are within this block. 58 * 59 * Step 2. 60 * If the cache handle is null then create a new handle and set the local copy. If any 61 * clear cache events are now called it"ll clear the handle and the next call will reload the 62 * cache but this call will continue with the local handle. 63 * 64 * Step 3. 65 * If what the handle points to is null then do the Slow search and set the local handle. 66 * There is some question on whether we should sync the setting of the local handle, I 67 * believe not as another thread not getting the new value (which is flushed fairly frequently) 68 * would just result in another search. If the object"s version of the handle hasn't been 69 * cleared i.e. same as the local version it is now set. 70 * 71 * Step 4. 72 * Return the value of the local handle which will never be null. 73 */ 74 public Thing getCacheThing() throws Exception 75 { 76 Thing holder[] = null; 77 78 // OPTIONAL A: Safer if sync block is here 79 synchronized( this)// Step 1. 80 { 81 holder = cacheThing; 82 83 if( holder == null) 84 { 85 // OPTIONAL B: Faster if the sync block is here. ( must have A or B) 86 holder = new Thing[1]; 87 88 cacheThing=holder; // Step 2 89 } 90 } 91 92 if( holder[0] == null) // Step 3. 93 { 94 DBQuery q = new DBQuery( Thing.DBCLASS_NAME, getDS()); 95 96 q.addClause( /* A complex/slow search criteria */); 97 98 DBObject obj = q.findOne(); 99 // Enter a new synchronized block to prevent reordering of instructions 100 synchronized( this) 101 { 102 holder[0] = obj; 103 } 104 } 105 106 return holder[0]; // Step 4. 107 } 108 109 /** 110 * A dependent of Bits has been added. This may effect the secondary cache so we should clear it. 111 * 112 * @param addedKey The dependent added 113 * @param sourceFieldKey The field that points to this object 114 */ 115 public void eventDependantAdded( GlobalKey addedKey, GlobalKey sourceFieldKey) 116 { 117 clearCache( addedKey); 118 } 119 120 /** 121 * A that we are watching has been changed 122 * 123 * @param obj The DBObject that was reload. 124 */ 125 public void eventReload( DBObject obj ) 126 { 127 clearCache( obj.getGlobalKey()); 128 } 129 130 /** 131 * A dependent of Bits has been removed. This may effect the secondary cache. 132 * 133 * @param removedKey The DBObject was removed. 134 * @param sourceFieldKey The linked field 135 */ 136 public void eventDependantRemoved( GlobalKey removedKey, GlobalKey sourceFieldKey) 137 { 138 clearCache( removedKey); 139 } 140 141 /** 142 * We should only clear the cache if the record changed could have possibly effected the cache. 143 * This method we be called MANY times. So it is cheaper just to clear the cache if in any doubt. 144 * Eg. If you are holding the primary security for this Company and the class of the changed 145 * object is "security" don"t go selecting it here to work out if you should clear it or not. 146 * 147 * This is automatically called by eventDataLoaded() in DBObject which does a programmer check 148 * that you have call the super.clearCache( changedKey); 149 */ 150 protected void clearCache( GlobalKey changedKey) 151 { 152 super.clearCache( changedKey); 153 154 if( /* only clear if the changed object effects the secondary cache */) 155 { 156 synchronized( this)// minimize the time we spend in synchronized blocks 157 { 158 cacheThing = null; 159 } 160 } 161 } 162 163 private Thing[] cacheThing; 164 }

Read More

Support for Search Engine Friendly URLs?
11 Apr 2013

Article titled with non URL friendly characters will be automatically translated to a friendly path when published. http://en.wikipedia.org/wiki/Clean_URL http://www.seochat.com/c/a/search-engine-optimization-help/tools-for-seo-search-engine-friendly-urls/

Read More

stSoftware's CloudBlocks
6 Apr 2013

We are highly experienced in successfully planning and implementing custom cloud and web systems for a variety of organizations, including those with complex data requirements. CloudBlocks universal modules and full range of enterprise level tools provide a substantial head start, reducing costs, time and risk "Our decision to use the ST Engine (CloudBlocks) as the foundation for our system proved to be cost-effective both in terms of development time and resources. ST enabled us to meet our aggressive...

Read More

How to work around DNS (Domain Name System) outages.
27 Mar 2013

On 28 of March 2013 the web's DNS was under attack by one of the largest DDoS attack in the web's history. http://www.abc.net.au/news/2013-03-28/huge-cyber-attack-slows-internet-globally/4598574 The DNS is used by a web browser to translate a host name to a IP address, which is a bit like using a phone book to look up a person's phone number. There are a number of ways of working around a DNS outage:- Change the DNS servers used This can be done on the Router setup This can be done on the browser...

Read More

What software is used to build stSoftware systems?
27 Mar 2013

Web Frameworks used:- Twitter bootstrap jQuery GWT TinyMCE elFinder Code Mirror Server frameworks:- Apache ActiveMQ Apache Commons Apache Jackrabbit Apache JAMES Apache JSPWiki Apache log4j Apache Lucene Core Apache POI Apache Xalan for Java XSLT Processor Apache Xerces for Java XML Parser Jasper Reports Build control:- Source control: CVS & SVN Issue tracking: Integrated into base system. Build control: Ant/Mavin Automated testing: JUnit Languages Used: Java 7, HTML5, JavaScript Supported Database...

Read More

Release Notes for Job Track - September 2012
26 Mar 2013

Release Overview Enhancements: - MYOB Synchronisation is here for Contacts!This is the first of many MYOB V19 integration enhancements which will see manual exports replaced with automated synchronisation. - WebDAV performance has been improved, see how easy it is to transfer files between your local machine and your online database with DRAG & DROP useability! Bug Fixes: - Auto Completer field refined specifically for contacts which have titles; Mr, Mrs, Ms, Dr... Enhancements - MYOB Synchronisation...

Read More

Our Mission
25 Mar 2013

Our mission is to create inspiring technology that improves the world. These are big goals but they’re goals we believe are worth striving for. Each day, we apply passion, strategy and focus in our work and in doing so, we enjoy seeing the huge improvements in ease of functionality and system reliability that our solutions generate for our clients. We hope to have the opportunity to offer you these same advantages and we would be pleased to discuss your requirements in further detail with you. Contact...

Read More

Our Team
25 Mar 2013

The stSoftware leadership team has high-level technical skills and capability in all aspects of solution design and development, legacy system migration and project management. We can use our knowledge, our skills and passion to leverage technology to generate significant operational improvements for your organisation. We’re also very ‘hands-on’ in our approach, and aim to develop a strong collaborative partnership with each of our clients. Our clients comment that we’re responsive to their requirements...

Read More

Values
25 Mar 2013

The team at stSoftware are driven by a series of values that underpin all we do. We strive to: We do this by: Be innovative Think about the essence of a problem to guide you to the best outcome Use a simple solution if it works as well as a complex one Invent a solution if one doesn’t exist Good procedures and backups allow you to be more creative You’re smart so show it in your work and try to be inspiring Build sustainable value Design code with the greatest longevity in mind Write documentation...

Read More