wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

From Blogsphere to a Static Site (Part 5) - Comment front-end


In Part 4 I described the comment backend. This installment sheds a light on the comment front-end.

Comments can be tricky. One lesson I learned early: When your comment form is standard HTML form, it attracts spam like a light bulb attracts moths. So the requirement were:

  • The original blog entry should not contain any HTML form. It should be loaded on a button click using JavaScript. Nota bene: this isn't hide/show, but actual manipulation of the DOM
  • The dynamic form shall not contain a POST URL, but submission should be in JavaScript - keeps a lot of the scumbags out already
  • Submission should be secured with a Captcha
  • Some formatting should be allowed. I opted for a Markdown editor with preview capabilities

The first component is the placeholder for the existing comments and the button showing the comment form:

<a name="comments"></a>
{{^commentsclosed}}
<div class="well well-raised" style="text-align : center">
  <button class="btn btn-lg btn-info" data-toggle="collapse" data-target="#commentform_{{UNID}}" type="button">
    Add your comment...  <span class="glyphicon glyphicon-comment"></span>
  </button>
</div>
<div id="commentform_{{UNID}}" class="collapse"></div>
{{/commentsclosed}}
<div class="well well-raised">
  <h4>Comments</h4>
  <ol id="commentList">
    {{#comments}}
    <li>
      {{#gravatarURL}}<img src="{{.}}" class="gravatarimg" /> {{/gravatarURL}} posted by <b>{{author}}</b> on <i>{{createdString}}</i>:
      <br /> {{& comment}}
      <hr style="clear : both" />
    </li> {{/comments}} {{^comments}}
    <li id="nocomments">
      <h5>No comments yet, be the first to comment</h5>
    </li>
    {{/comments}}
  </ol>
</div>

The second component is the comment form, implemented as mustache template - one of the reasons I picked Mustache: runs on the server and the client in tons of languages

<form title="Comment form for blog discussion" onSubmit="return addComment(this,'{{recaptchaid}}','{{parentId}}')" class="form-vertical well well-raised">
  <fieldset>
    <legend>Add your comment</legend>
    <p>Please note: <b>Comments without a valid and working eMail address will be removed.</b>
      <br /> This is my site, so I decide what stays here and what goes.</p>
     <div class="control-group" id="commentcontrol">
      <label class="control-label" for="Commentor">Name (required, published)</label>
      <div class="controls">
        <input class="input-xlarge focused" id="Commentor" size="30" accesskey="n" name="nameAuthor" />
      </div>
      <label class="control-label" for="Email">eMail (required, not published)</label>
      <div class="controls">
        <input type="eMail" class="input-xlarge focused" id="Email" size="30" accesskey="n" name="txtEmail" placeholder="A working eMail please!" />
      </div>
      <label class="control-label" for="webSite">URL (optional)</label>
      <div class="controls">
        <input type="url" class="input-xlarge" id="webSite" size="30" accesskey="n" name="txtWebSite" />
      </div>
      <div class="controls">
        <div id="wmd-panel" class="wmd-panel">
          <table style="width : 100%" border="0">
            <tr>
              <td style="width : 50%; vertical-align : top">
                <label class="control-label" for="wmd-input">Your Comment (Use markdown like <a href="//stackoverflow.com/editing-help" target="_blank">Stackoverflow</a>)</label>
                <div id="wmd-button-bar"></div>
                <textarea class="wmd-input" id="wmd-input" name="Body"></textarea>
              </td>
              <td style="width : 50%; vertical-align : top">
                <label class="control-label">Preview</label>
                <div id="wmd-preview" class="wmd-panel wmd-preview"></div>
              </td>
            </tr>
          </table>
        </div>
      </div>
      <div class="controls" id="captchadiv">Captcha here</div>
      <div class="form-actions">
        <button id="commentsubmit" type="submit" class="btn btn-primary btn-large">Post your comment</button>
      </div>
    </div>
    <div class="alert alert-block" id="alertContainer" style="display : none">One moment please, submitting comment...</div>
  </fieldset>
</form>

The whole mechanism gets to work with just a few jQuery JavaScript functions (Vanilla JS would work too, but I had JQuery already for the social buttons, so I reused that) and the respective JS files:


Read more

Posted by on 2017-07-20 10:58 | Comments (0) | categories: Blog

Ownership flow of customer community created cases


In a previous entry I mapped out sharing abilities the Salesforce platform is capable of.

In this entry I will put these capabilities to practial use. The scenario starts with a customer logging into the customer community and raising a ticket. Client representative, the partner and support staff need/want access to the case. The case needs to flow along the same processing as cases created by other means (call, email, IoT). The following diagram explains the flow (click for a larger version):

Customer Portal Created Cases Ownership flow

The diagram has been rendered using Websequencediagrams based on a plain text file:


Read more

Posted by on 2017-07-16 09:53 | Comments (0) | categories: Salesforce

Using FontAwesome in your VisualForce pages


I'm a big fan of Font Awesome (including backing Version 5). It allows to add icons without the overhead of loading tons of small graphic files. In a recent internal project I had to integrate FontAwesome into a Salesforce VisualForce page

Installing FontAwesome

In theory you could load the font from a CDN. However based on security settings this might or might not work. So I decided to add FontAwesome as resource to my project:

  1. Download FontAwesome 4. Or support the team and use Version 5
  2. Add the zip file as static resource to your setup:
    Static Resource for FontAwesome
  3. Make sure you have set the cache control to public, so the font doesn't need download after first use.

Adding to the VisualForce Page

The font gets added using a stylesheet that is included in the zip file added as a resource in the previous step. the apex:stylesheet element takes care of that. The only catch: the zip file is internally "expanded" OS/X style with the file name (sans extension) as directory name. So the tag needs to look like this:

  <apex:stylesheet value="{!URLFOR($Resource.fontawesome,'/font-awesome-4.7.0/css/font-awesome.css')}"/>

Using Icons

This follows the standard approach: you create an empty &lt;i&gt; element and use the class property to specify icon and options. Like this:

  <i style="color: red" class="fa fa-user-circle fa-2x" aria-hidden="true"></i>

Make sure you have a look at the full selection of icons available.

As usual: YMMV


Posted by on 2017-07-14 08:43 | Comments (0) | categories: Salesforce

Salesforce Community License Overview


To facilitate collaboration across corporate boundaries, Salesforce offers Salesforce Communities. The licence for communities come in three flavours: customer community, customer community plus and partner community. There are similarities and differences between the entitlements.

The biggest difference can be seen between customer community and customer community plus. There's a simple rationale behind it: a customer community account uses simplified sharing access, so the computational load to determine access of potentially millions of users stays reasonable and easy to grasp for administrators:

Community License Overview

All licences, full Salesforce or one of the community licence types, access the same data, in the confines of the entitlements. So it's NOT different products, just different permissions on the Salesforce platform.


Posted by on 2017-07-10 09:40 | Comments (0) | categories: Salesforce

Sharing is caring - Salesforce edition


I like declarative security to give access exactly to the extend a user requires. When learning Salesforce, I discovered to my delight the portfolio of possibilities to tailor access. With great powers, as we know, come great responsibilities. Learning the Salesforce lingo can be quite daunting. So here is my little overview:

Salesforce Sharing Terminology, click for full size
  • Access to data in Salesforce is based on two principles: everything is owner based and a generally restricted access can be extended for given conditions, but not limited. This single vector of access makes systems cleaner that the ability to add and remove privileges based on conditions. It avoids the need to resolve conflicts where condition 1 gives access, while condition 2 would remove it. These conflict solution rules are a security flaw in waiting (prime vector for human error). Nevertheless access in Salesforce need to be well planned (How much does any role need to see: give to little and you invite data duplicates, give too much and you increase leakage risks) - How to plan is another story for another time
  • Access has 3 element: access to objects (that would be access to classes in OO), access to records (instances of a class) and fields (properties of a class instance)
  • Computation starts from the organization wide settings, which are the most restrictive settings for a given organisation and then gets extended with various means (see image above)
  • There's a general distinction between internal access and access via a community. This reflects the need to be able to interact with customers, suppliers and partners in a controlled fashion
  • Owner based: I've seen this quite often: data exists, gets used, but nobody wants to own it, the owner has left or data gets inaccessible when the owner gets deleted. All this issues don't happen in Salesforce since no object data can exist that doesn't have an owner and owner transfer capability is baked into the platform (even rule based, but that's another story for another time)
  • Hierarchical: access rules know the role and reporting hierarchy. So access can be granted to a user and her entire reporting hierarchy including subordinates etc. Quite extensive possibilities worth exploring

As usual YMMV


Posted by on 2017-07-07 08:52 | Comments (0) | categories: Salesforce

Meeting a CxO


Meeting a CxO
These are my notes on a role play we did in Salesforce to better cater to successful CxO meetings. Most of the topics do apply to any meeting, so no surprises here. We had actual CxO as participants. They shared that the most common item wanting is clear meeting agendas with outcomes, including them being time managed during the meeting. Second in line were unclear outcomes and not asking for a follow-up meeting.

Posted by on 2017-06-08 09:28 | Comments (0) | categories: Salesforce

From Blogsphere to a Static Site (Part 4) - Comment backend


The blog needed a comment function. While there are social options around (Facebook, Disqus etc), I decided I want to roll my own. Partly because I want tighter control and partly, well, because I could. My comment backend would:

  • Provide a REST API to create comments in a JSON structure. The comment body will be Markdown. Reading would provide comments in ready to use HTML (I hear howling from the API crowd). No delete or update functionality
  • Cleanup content considered harmful (code injection) and optional sport Captcha
  • Store all content in a NoSQL database, in my case CouchDB (or Cloudant with its 20G free plan)
  • Cache all queries for comment in an online cache to limit calls to the database
  • Initially run on Domino, later on liberty or the raw JVM
  • Initially also update Domino using a web service - so during transition no comments would get lost

In its initial incarnation the Comment servlet is a OSGi plugin that listens to the /comments URL implemented as Wink servlet. So the class of interest is the one defining the service. We have one method for post, one for get and a helper function

/**
 * Wink implementation of Comment service
 */
@Workspace(workspaceTitle = "Blog Comments", collectionTitle = "Create or display comments")
@Path(value = "/comments")
@Produces(MediaType.TEXT_HTML)
@Consumes(MediaType.APPLICATION_JSON)
public class CommentService extends CommentResponse {

    private final Logger logger = Logger.getLogger(this.getClass().getName());

    @POST
    public Response createComment(@Context HttpServletRequest request) {
        final Monitor mon = MonitorFactory.start("CommentService#createComment");
        String result = "Sorry I can't process your comment at this time";
        ResponseBuilder builder = Response.ok();

        try {
            InputStream in = request.getInputStream();
            BlogComment comment = BlogComment.load(in);
            in.close();

            if (comment != null) {
                this.captureSubmissionDetails(request, comment);
                result = CommentManager.INSTANCE.saveComment(comment, true);
            } else {
                builder.status(Status.NOT_ACCEPTABLE);
            }
            builder.entity(result).type(MediaType.TEXT_HTML_TYPE);

        } catch (Exception e) {
            String errorMessage = e.getMessage();
            builder.entity((((errorMessage == null) || errorMessage.equals("")) ? "Undefined error" : errorMessage)).type(
                    MediaType.TEXT_HTML_TYPE);
            Utils.logError(this.logger, e);
        }

        mon.stop();
        return builder.build();
    }

    @GET
    public Response getComments(@QueryParam("parentid") final String parentid) {
        Response response = null;
        final Monitor mon = MonitorFactory.start("CommentService#getComments");
        final ResponseBuilder builder = Response.ok();
        final Collection<BlogComment> bc = CommentManager.INSTANCE.loadComments(parentid);

        if ((bc == null) || bc.isEmpty()) {
            builder.status(Status.NO_CONTENT);
        } else {
            response = this.renderOutput(bc, "comment.mustache");
        }
        mon.stop();
        return (response == null) ? builder.build() : response;
    }

    private void captureSubmissionDetails(HttpServletRequest request, BlogComment comment) {
        final Monitor mon = MonitorFactory.start("CommentService#captureSubmissionDetails");
        try {
            @SuppressWarnings("rawtypes")
            Enumeration hn = request.getHeaderNames();
            if (hn != null) {
                while (hn.hasMoreElements()) {
                    String key = hn.nextElement().toString();
                    comment.addParameter(key, request.getHeader(key));
                }
            }
            @SuppressWarnings("rawtypes")
            Enumeration pn = request.getParameterNames();
            if (pn != null) {
                while (pn.hasMoreElements()) {
                    String key = pn.nextElement().toString();
                    String[] values = request.getParameterValues(key);
                    comment.addParameters(key, values);

                    if (key.equals("referer")) {
                        comment.setReferer(values[0]);
                    } else if (key.equals("user-agent")) {
                        comment.setUserAgent(values[0]);
                    }

                }
            }
            @SuppressWarnings("rawtypes")
            Enumeration an = request.getAttributeNames();
            if (an != null) {
                while (an.hasMoreElements()) {
                    try {
                        String key = an.nextElement().toString();
                        comment.addAttribute(key, String.valueOf(request.getAttribute(key)));
                    } catch (Exception e) {
                        // No action here
                    }
                }
            }
            comment.addParameter("REMOTE_HOST", request.getRemoteHost());
            comment.addParameter("REMOTE_ADDR", request.getRemoteAddr());
            comment.addParameter("REMOTE_USER", request.getRemoteUser());

            // Needed for Captcha
            comment.setRemoteAddress(request.getRemoteAddr());
        } catch (Exception e) {
            Utils.logError(this.logger, e);
            // But no further action here!
        }
        mon.stop();
    }
}

Read more

Posted by on 2017-05-04 07:32 | Comments (2) | categories: Blog

The Decline and Fall of IBM


I wrote this quite a while ago, never finished the article until now. Enjoy.

Yeah right!

With this words Robert X Cringley a.k.a Mark Stephens celebrates himself when he asserts something clever. His book The Decline and Fall of IBM created quite some stir and was a hot topic of discussion especially among IBMers and alumni.
So I got myself a copy and had a look. Clearly he has an axe to grind with IBM and everybody is invited. Many came. Half of the book consists of mostly grieving comments ranging from 2007 until 2014. With all this contributions, the content remains light on substance. After all it costs you less than a late.

Some of the stated observations are spot on, like "IBM is a sales organisation", others while looking like observations, but rather are opinions (e.g. "IBM lost its way") and statements that made me feel: "why didn't the IBM board ask him to run IBM? He seems to know so much better!", exactly like an arbitrary spectator of the world cup in a pub can tell you what a team did wrong.

I'm not saying, that all is well in IBM, that would be a fools view. An organisation with a size and workforce exceeding several countries does live in challenging times. An economic system, that values growth over everything is problematic (In biology an organism that grows indefinitely is called cancer) at least and transiting to a global workforce on this scale is unparalleled. I share his view that the dance around the golden calf known as shareholder value might be praying to a false good.
Here are some comments to Cringley's statements, that crossed my mind (paraphrasing):

  • Cringley: "In IBM management is royalty, the sales force the nobility and technical people are the peasants. No peasant can dream to become royalty".
    Yeah right! IBM fellows, IBM Distinguished Engineers or members of the IBM Academy of Technology will disagree. The irony here: in the "good old IBM (of Watson)" there was no career path for technical experts. In the IBM of today there is
  • I worked a lot with our engineers in India and China. They are hard working, ready to learn and, by now, quite experienced. Asserting that they are less capable that their American counterparts, seems quite arrogant to me. Yes, they were unexperienced a decade ago, but that's a long time in IT years. Also: there are capable and incapable engineers everywhere. Pinning it on a specific country or region is (insert your own statement of backwards here). A real issue however are IBM's processes, that had been designed to cater to get less experienced people on board. They need an overhaul.
  • Cringley: "IBM should not sell the Intel servers to Lenovo".
    Yeah right! Cringley portrays it as the complete exit out of a server growth market. However IBM still has Intel based technology in their PureSystems and acquired skills and know how through the Softlayer acquisition how to build the special segment of Intel based machines that run in cloud size data centres. So instead of exiting a segment, it looks to me like eliminating duplicate product lines
  • Cringley: "IBM should port AIX to Intel".
    Yeah right! AIX runs on one class of IBM machines (System P), while Linux runs on everything from Softlayer Bluemix to Mainframe. Linux outperforms AIX on System P in quite some workloads. So what makes a better investment? Port AIX or infuse the security know how of AIX into Linux? (the file system options are there already)
  • Cringley: "IBM should ditch the Power architecture and switch to Intel"
    Yeah right! IBM has vast know how in building processors that will get lost when stopping to develop them. Asset utilisation isn't ditching assets, but making them more competitive. Intel and ARM aren't the only shops who can design processors, just have a look at OpenPOWER
  • Cringley: Hadoop will make mainframes obsolete.
    Yeah right! The technology is around for a decade, Google, its inventor, already moved on. Asserting a single technology will kill the I/O beasts known as Big Iron, looks like an inflight magazine statement. Besides the fact, that IBM offers a robust Hadoop implementation for a while already, which you can run on zLinux, if you choose so.
  • The book contains quite some areas, like current staff morale, the process culture or the layers of management that highlight pain. However where he is wrong: IBM isn't blind to the issues and there are forces inside working for the betterment (even if there is a case of a Knowing-Doing-Gap)
Of course, YMMY, so read it for yourself

Posted by on 2017-05-01 12:35 | Comments (0) | categories: IBM

From Blogsphere to a Static Site (Part 3) - Generating pages


The rendering engine I choose is mustache which describes itself as "logic-less templating engine". My main criteria was the availability on multiple platforms including Java and JavaScript (I might port the rendering part to NodeJS at some time in the future).
The only logic mustache supports is conditional rendering based on the presence or absence of an element. When an element is present and is an array (or a collection in Java) the body of the template gets repeated for each element in the array. A scalar value hence is treated as an array with one value only.

Mustache is simple to use. All you need is a data bean (in Java, a JSON structure for JavaScript) and a text file containing placeholders with the property names. E.g. <h1>{{title}}</h1> will render a headline with the title property of you data object. In Java that would be either a public variable or a call to getTitle according to the bean specification. The blog rendering code therefore is quite simple:

private void renderOneEntry(BlogEntry be, Mustache mustache) throws IOException {

        String location = this.config.destinationDirectory + be.getNewURL();
        String outDirs = location.substring(0, location.lastIndexOf("/"));
        File dirs = new File(outDirs);
        if (!dirs.exists()) {
            dirs.mkdirs();
        }
        // Set the current context
        for (LinkItem cat : be.getCategory()) {
            String c = cat.name;
            this.allCategories.get(c.toLowerCase()).active = true;
        }
        this.allDateCategories.get(be.getDateYear()).active = true;

        if (be.getSeries() != null) {
            String series = be.getSeries();
            if (this.allSeries.containsKey(series)) {
                this.allSeries.get(series).get(be.getNewURL()).active = true;
            }
        }

        // Prepare to write out
        ByteArrayOutputStream out = new ByteArrayOutputStream(102400);
        Writer pw = new PrintWriter(out);

        // This is where the magic happens
        mustache.execute(pw, be);
        pw.flush();
        pw.close();
        this.saveIfChanged(out.toByteArray(), location);

        // Cleanup
        for (LinkItem cat : be.getCategory()) {
            String c = cat.name;
            this.allCategories.get(c.toLowerCase()).active = false;
        }
        this.allDateCategories.get(be.getDateYear()).active = false;

        if (be.getSeries() != null) {
            String series = be.getSeries();
            if (this.allSeries.containsKey(series)) {
                this.allSeries.get(series).get(be.getNewURL()).active = false;
            }
        }
    }

The actual rendering is just the line mustache.execute(pw, be); The code around it prepares and resets the collections that might render on a page like categories, series or month and year. Also of interest is this.saveIfChanged(out.toByteArray(), location); which only saves results back to disk if it actually has changed. Don't be mistaken: any change in layout will lead to a newly rendered page, so this is quite important to save as needed and not more (you don't want to have tons of identical files that only differ in their time stamp)


Read more

Posted by on 2017-05-01 11:23 | Comments (0) | categories: Blog

From Blogsphere to a Static Site (Part 2) - Cleaning up the HTML


Blogsphere allows to create RichText and plain HTML entries. To export them I need to grab the HTML, either the manual entered or the RichText generated on, clean it up (especially for my manual entered HTML) and then replace image sources and internal links using the new URL syntax. To make this happen I created 2 functions that saved images and attachments and created a lookup list, so the HTML cleanup has a mapping table to work with

private void saveImage(Document doc) {
        String sourceDirectory = this.config.sourceDirectory + this.config.imageDirectory;
        try {
            String subject = doc.getItemValueString("ImageName");
            Date created = doc.getCreated().toJavaDate();
            @SuppressWarnings("rawtypes")
            Vector attNames = this.s.evaluate("@AttachmentNames", doc);
            String description = doc.getItemValueString("ImageName");
            String oldURL = this.config.oldImageLocation + doc.getItemValueString("ImageUNID") + "/$File/";
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy");
            String year = sdf.format(created);
            FileEntry fe = this.imgEntries.add(subject, oldURL, description, created);

            for (Object attObj : attNames) {
                try {
                    String attName = attObj.toString();
                    String newURL = this.config.webBlogLocation + this.config.imageDirectory + year + "/" + attName;
                    fe.add(attName, newURL, description, created);
                    String outDir = sourceDirectory + year + "/";
                    this.ensureDirectory(outDir);
                    EmbeddedObject att = doc.getAttachment(attName);
                    att.extractFile(outDir + attName);
                    Utils.shred(att);
                } catch (NotesException e) {
                    e.printStackTrace();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }

        } catch (NotesException e) {
            e.printStackTrace();
        }

    }

    private void saveImageFromURL(String href, String targetName) {

        String fetchFromWhere = "https://" + this.config.bloghost + href;
        try {
            byte[] curImg = Request.Get(fetchFromWhere).execute().returnContent().asBytes();
            this.saveIfChanged(curImg, targetName);
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

With images saved the HTML cleanup can proceed. As mentioned before I'm using JSoup to process crappy HTML. It allows for easy extraction of elements and attributes, so processing of links an images is just a few lines


Read more

Posted by on 2017-04-17 09:29 | Comments (0) | categories: Blog