NOW THAT WE VE COVERED the basics of ATL Server SRF files and request handlers, we can examine more advanced areas; in particular, extending the stencil syntax for custom tags. Familiarity with the ATL Server stencil extensibility mechanisms will provide you with the tools to work around many of the issues and limitations you may encounter in writing your Web applications.
In this section we examine how to extend the ATL Server stencil syntax by adding custom tags and modifying the way existing tags work. We show you how to add several new control-flow tokens to demonstrate how you can override and extend the stencil syntax. You ll add the following control-flow tags:
if_and : Same as the if tag, but the if block is entered only if all the statements in the conditional are true (hence and )
if_or : Same as the if tag, but the if block is entered if any of the statements in the conditional are true (hence or )
if_not : Same as the if tag, but the if block is entered only if the statement in the conditional is not true
Additionally, you ll add support to the if_and and if_or tags for not statements using the ! syntax. That is, {{if_and !tag1 tag2}} evaluates to true only if tag1 is not true and tag2 is true . You ll also extend the else and endif tags to support the new conditional tags.
The ATL Server framework breaks up stencil processing into two parts : parsing the stencil and rendering the stencil. As we explained earlier, this parse-once model avoids the costs of parsing the stencil on every page hit. The top-level parsing method is CStencil::ParseReplacements , which in turn calls CStencil::ParseToken for each stencil tag that s encountered . You ll override ParseToken to handle your new tags and extend the else and endif tags. You ll also need to override CStencil::FinishParseReplacements to check for unclosed if blocks and other errors in the same way as the base CStencil class does.
The rendering of the stencil is handled in the CStencil::Render function, which in turn calls CStencil::RenderToken for each stencil token. You ll override RenderToken to handle the rendering of your new tags.
The first thing you need to do is define constants to represent your new tags:
#define STENCIL_ANDSTART (STENCIL_USER_TOKEN_BASE + 0x00000001) #define STENCIL_ANDPART (STENCIL_USER_TOKEN_BASE + 0x00000002) #define STENCIL_ORSTART (STENCIL_USER_TOKEN_BASE + 0x00000003) #define STENCIL_ORPART (STENCIL_USER_TOKEN_BASE + 0x00000004) #define STENCIL_IFNOT (STENCIL_USER_TOKEN_BASE + 0x00000005)
Note that all the tags are based from STENCIL_USER_TOKEN_BASE , which is defined by the ATL Server framework and is meant to be used as a base for custom tags.
The next thing you need to do is derive a class from CStencil to handle your new tags. Because by default ATL Server request handlers derive from CHtmlStencil (which in turn derives from CStencil ), you ll derive from CHtmlStencil . You ll call your new class CCustomStencil .
class CCustomStencil : public CHtmlStencil { public: CCustomStencil(IAtlMemMgr *pMemMgr) throw() :CHtmlStencil(pMemMgr) { } // omitted };
Then you need to override ParseToken to understand your new tags and override the behavior of the endif and else tags. You ll use the ATL Server support functions SkipSpace and CheckTag to verify your tags, as shown in Listing 17-1. If the token doesn t match one of your new tags or the tags you need to override, you ll delegate to the base class.
virtual PARSE_TOKEN_RESULT ParseToken(LPSTR szTokenStart, LPSTR szTokenEnd, DWORD *pBlockStack, DWORD *pdwTop) throw() { LPCSTR pStart = szTokenStart; pStart += 2; //skip curlies pStart = SkipSpace(pStart, GetCodePage()); DWORD dwLen = (DWORD)(szTokenEnd - szTokenStart); int nIndex = -1; if (CheckTag("if_and", sizeof("if_and")-1, pStart, dwLen)) nIndex = ParseAnd(szTokenStart, szTokenEnd, pBlockStack, pdwTop); else if (CheckTag("if_or", sizeof("if_or")-1, pStart, dwLen)) nIndex = ParseOr(szTokenStart, szTokenEnd, pBlockStack, pdwTop); else if (CheckTag("endif", sizeof("endif")-1, pStart, dwLen)) nIndex = ParseEndEx(szTokenStart, szTokenEnd, pBlockStack, pdwTop); else if (CheckTag("else", sizeof("else")-1, pStart, dwLen)) nIndex = ParseElseEx(szTokenStart, szTokenEnd, pBlockStack, pdwTop); else if (CheckTag("if_not", sizeof("if_not")-1, pStart, dwLen)) nIndex = ParseIfNot(szTokenStart, szTokenEnd, pBlockStack, pdwTop); else { // delegate to base class return __super::ParseToken(szTokenStart, szTokenEnd, pBlockStack, pdwTop); } if (nIndex < 0) return INVALID_TOKEN; return RESERVED_TOKEN; }
Now let s look at what you need to do implement one of these new tags in more detail. You ll take the simplest example first: if_not . All you need to do in this case is delegate to the ATL Server support function ParseReplacement with your custom token type STENCIL_IFNOT , as shown in Listing 17-2.
bool FinishParseReplacements() throw() { DWORD dwSize = GetTokenCount(); // iterate over the tokens for (DWORD dwIndex = 0; dwIndex < dwSize; dwIndex++) { StencilToken& token = *(GetToken(dwIndex)); // only modify the new tags bool bModify = (token.type == STENCIL_ANDPART token.type == STENCIL_ORPART token.type == STENCIL_IFNOT token.type == STENCIL_ANDSTART token.type == STENCIL_ORSTART); // unclosed blocks are conditional start // tags with the dwLoopIndex == STENCIL_INVALIDOFFSET bool bUnclosedBlock = ((token.type == STENCIL_ANDSTART token.type == STENCIL_ORSTART token.type == STENCIL_IFNOT) && token.dwLoopIndex == STENCIL_INVALIDOFFSET); if (bModify && token.szMethodName[0] && (token.dwFnOffset == STENCIL_INVALIDOFFSET bUnclosedBlock)) { // if it is an unclosed block or if we cannot resolve the // replacement method, convert to text tags if (bUnclosedBlock m_pReplacer->FindReplacementOffset(token.szMethodName, &token.dwFnOffset, token.szHandlerName, &token.dwObjOffset, &token.dwMap, (void **)&token.dwData, m_pMemMgr) != HTTP_SUCCESS) { DWORD dwStartIndex = dwIndex; // if the token is STENCIL_ANDPART or STENCIL_ORPART, // find the start of the block if (token.type == STENCIL_ANDPART token.type == STENCIL_ORPART) { DWORD dwStartType = (token.type == STENCIL_ANDPART ? STENCIL_ANDSTART : STENCIL_ORSTART); DWORD dwStartIndex = dwIndex-1; while (IsValidIndex(dwStartIndex) && GetToken(dwStartIndex)->type != dwStartType) { --dwStartIndex; } if (GetToken(dwStartIndex)->type != dwStartType) dwStartIndex = dwIndex; } DWORD dwLoopIndex = GetToken(dwStartIndex)->dwLoopIndex; // if STENCIL_ANDSTART or STENCIL_ORSTART, // convert component tokens to STENCIL_CONDITIONALEND, // so they are skipped during rendering and set // szMethodName to an empty string, so it is ignored by // the base class's FinishParseReplacements if (GetToken(dwStartIndex)->type == STENCIL_ANDSTART GetToken(dwStartIndex)->type == STENCIL_ORSTART) { DWORD dwNextToken = dwIndex+1; while (dwNextToken != dwLoopIndex && dwNextToken != STENCIL_INVALIDINDEX) { if (GetToken(dwNextToken)->type == STENCIL_ANDPART GetToken(dwNextToken)->type == STENCIL_ORPART) { GetToken(dwNextToken)->type = STENCIL_CONDITIONALEND; GetToken(dwNextToken)->szMethodName[0] = 0; } dwNextToken++; } } // unresolved replacement, convert it to a text token GetToken(dwStartIndex)->type = STENCIL_TEXTTAG; GetToken(dwStartIndex)->szMethodName[0] = 0; // convert all linked tokens to text tokens as well // this includes: endif, else, endwhile while (dwLoopIndex != dwIndex && dwLoopIndex != STENCIL_INVALIDINDEX) { GetToken(dwLoopIndex)->type = STENCIL_TEXTTAG; GetToken(dwLoopIndex)->szMethodName[0] = 0; dwLoopIndex = GetToken(dwLoopIndex)->dwLoopIndex; } } } } // call the base FinishParseReplacements to handle other tags return __super::FinishParseReplacements(); }
To render the new tags, override the RenderToken function, as shown in Listing 17-3. This function will check the token type and then delegate to RenderAnd , RenderOr , and RenderIfNot . If the token type isn t one of your new tokens, delegate to the base class implementation.
DWORD RenderToken(DWORD dwIndex, ITagReplacer* pReplacer, IWriteStream *pWriteStream, DWORD *pdwErrorCode, void* pState = NULL) const throw() { DWORD dwNextToken = STENCIL_INVALIDINDEX; DWORD dwErrorCode = HTTP_SUCCESS; const StencilToken* ptoken = GetToken(dwIndex); if (ptoken) { switch(ptoken->type) { case STENCIL_ANDSTART: // render the {{if_and ...}} tag dwNextToken = RenderAnd(dwIndex, pReplacer, &dwErrorCode); break; case STENCIL_ORSTART: // render the {{if_or ...}} tag dwNextToken = RenderOr(dwIndex, pReplacer, &dwErrorCode); break; case STENCIL_IFNOT: // render the {{if_not ...}} tag dwNextToken = RenderIfNot(dwIndex, pReplacer, &dwErrorCode); break; default: // delegate to the base class dwNextToken = __super::RenderToken(dwIndex, pReplacer, pWriteStream, &dwErrorCode, (CStencilState *)pState); break; } } if (pdwErrorCode) *pdwErrorCode = dwErrorCode; return dwNextToken; }
Note that even though you override the parsing of the endif and else tags, you don t need to override their runtime behavior.
The RenderIfNot function is effectively the same as rendering normal if blocks with the exception of the evaluation of the condition that determines if the block is entered (see Listing 17-4).
DWORD RenderIfNot(DWORD dwIndex, ITagReplacer *pReplacer, DWORD *pdwErrorCode) const throw() { const StencilToken* ptoken = GetToken(dwIndex); DWORD dwNextToken = STENCIL_INVALIDINDEX; DWORD dwErrorCode = HTTP_SUCCESS; if (!ptoken) return STENCIL_INVALIDINDEX; // ensure the function is valid if (ptoken->type == STENCIL_IFNOT && ptoken->dwFnOffset == STENCIL_INVALIDINDEX) { dwErrorCode = HTTP_ERROR(500, ISE_SUBERR_STENCIL_INVALIDFUNCOFFSET); } else { // ensure that we end with an {{endif}} or an {{else}} if (ptoken->dwLoopIndex == STENCIL_INVALIDINDEX) { dwErrorCode = HTTP_ERROR(500, ISE_SUBERR_STENCIL_MISMATCHIF); } else { // points to the end of the loop DWORD dwLoopIndex = ptoken->dwLoopIndex; // Call the replacement method. // If it returns HTTP_S_FALSE, we render everything up to // the end of the conditional. // if it returns HTTP_SUCCESS, the condition is not met // and we render the else part if it exists or jump past // the endif otherwise DWORD dwErr = pReplacer->RenderReplacement(ptoken->dwFnOffset, ptoken->dwObjOffset, ptoken->dwMap, (void *)ptoken->dwData); if (dwErr == HTTP_S_FALSE) { // if the method returns false, execute the block dwNextToken = dwIndex+1; dwErrorCode = HTTP_SUCCESS; } else if (dwErr == HTTP_SUCCESS) { // if the method returns true, // jump to the end of the block dwNextToken = dwLoopIndex+1; dwErrorCode = HTTP_SUCCESS; } else { // otherwise, an error occurred dwNextToken = STENCIL_INVALIDINDEX; dwErrorCode = dwErr; } } } if (pdwErrorCode) *pdwErrorCode = dwErrorCode; return dwNextToken; }
For rendering if_and and if_or , you need to handle the multiple statements in the conditional as well as potential ! (not) conditions in the statement. You simply loop over the statements and call the replacement methods , as shown in Listing 17-5. Depending on whether it s an and or an or , you short-circuit on first success or first failure.
DWORD RenderAnd(DWORD dwIndex, ITagReplacer *pReplacer, DWORD *pdwErrorCode) const throw() { const StencilToken* ptoken = GetToken(dwIndex); DWORD dwNextToken = STENCIL_INVALIDINDEX; DWORD dwErrorCode = HTTP_SUCCESS; if (!ptoken) return STENCIL_INVALIDINDEX; // ensure that the function offset is valid if (ptoken->type == STENCIL_ANDSTART && ptoken->dwFnOffset == STENCIL_INVALIDINDEX) { dwErrorCode = HTTP_ERROR(500, ISE_SUBERR_STENCIL_INVALIDFUNCOFFSET); } else { // ensure that we end with and {{endif}} or and {{else}} if (ptoken->dwLoopIndex == STENCIL_INVALIDINDEX) { dwErrorCode = HTTP_ERROR(500, ISE_SUBERR_STENCIL_MISMATCHIF); } else { // points to the end of the loop DWORD dwLoopIndex = ptoken->dwLoopIndex; DWORD dwErr; // loop over the components of the if_and do { // call the replacement method dwErr = pReplacer->RenderReplacement(ptoken->dwFnOffset, ptoken->dwObjOffset, ptoken->dwMap, (void *)ptoken->dwData); // check if it is '! <method_name>' call if (ptoken->dwData) { // invert the return code if (dwErr == HTTP_S_FALSE) dwErr = HTTP_SUCCESS; else if (dwErr == HTTP_SUCCESS) dwErr = HTTP_S_FALSE; } // short-circuit on failure if (dwErr) break; // go to the next token dwIndex++; // ensure that the token is valid if (STENCIL_SUCCESS == (dwErrorCode = IsValidIndex(dwIndex))) { ptoken = GetToken(dwIndex); } else { dwErr = dwErrorCode; break; } // loop until we reach the end of the {{if_and ...}} methods } while (ptoken->type == STENCIL_ANDPART); if (!dwErr) { // if successful, dwIndex is the index of the // first token inside the {{if_and ...}} block dwNextToken = dwIndex; dwErrorCode = HTTP_SUCCESS; } else if (dwErr == HTTP_S_FALSE) { // if we failed, jump to the end of the block // ({{else}} or {{endif}}) // and increment by one dwNextToken = dwLoopIndex+1; dwErrorCode = HTTP_SUCCESS; } else { // an error has occurred dwNextToken = STENCIL_INVALIDINDEX; dwErrorCode = dwErr; } } } if (pdwErrorCode) *pdwErrorCode = dwErrorCode; return dwNextToken; }
You have now successfully added all the code necessary to parse and render your new tags. To use the new tags in an ATL Server request handler, you need to specify the stencil type, as shown in Listing 17-6.
class CMyHandler : public CRequestHandlerT< CMyHandler, CComSingleThreadModel, CHtmlTagReplacer<CMyHandler, CCustomStencil> > { // normal request handler code }; // class CMyHandler
Now CMyHandler can use the new tags in all SRF files that refer to it.