2008年8月22日 星期五

Software outsourcing

專注於核心項目,將非核心工作交付外包,等於間接提升企業的競爭能力,也節省了資源及時間。但是軟體外包因牽扯到眾多面向,導致實際的執行狀況非理論所能解釋,甚至對軟體外包所能帶來的效益產生質疑。為了能重新審視軟體外包的實務面,從中淬取出可供往後參考的寶貴經驗,茲將近期於工作中經歷的軟體外包案例重新審視,並針對幾項關鍵提出檢討。
  • 外包動機
    外包動機對於軟體外包來說是非常重要的一環,不管企業本身的規模大小與否,以及所能接受的外包成本多寡,都必須先審慎思考一件十分基礎的問題:是否真的該外包?所外包的內容也許是軟體系統,也許是軟體原件,只要外包所耗用之成本及時間效益不如企業自行開發來得便宜或快速,就不應該外包。然而實際的情況是,有太多的人為干擾因素存在於企業當中,導致軟體外包真正的意義被忽略,取而代之的是個人或部門績效。一個完善的組織應當避免此等狀況發生,否則即使拉高了個人或部門的績效,但是對公司整體而言卻是傷害。
  • 需求定義
    軟體外包案的品質優劣與否,跟需求定義的完善程度有絕對的正向關係。粗糙的需求定義即代表粗糙的成品,發包的企業往往會有角色錯置的問題,也就是以為外包廠商會以盡善盡美的態度來開發,這是一相情願的想法。首先,企業的文化絕對無法影響外包廠商,畢竟兩者是互為獨立的公司,再者,外包廠商總是以盡快完成交付案件並收取報酬為優先考量,跟處處斟酌細節、反覆推敲的公司內部開發流程截然不同,故軟體外包的需求定義必須力求精確、涵蓋完整,否則差強人意、甚至不堪使用的外包成品是必然結果。
  • 溝通與確認
    在執行外包的過程中,與外包廠商溝通的聯絡人或專案經理必須密集、精確的反覆確認外包案的需求定義,一方面避免外包案在最後驗收過程才來檢驗,一方面也對外包廠商傳達了督促的訊息,尤其是後者非常重要,否則外包廠商一定會刻意規避潛在的開發細節和問題。除了對外的溝通之外,對內與技術人員的反覆確認也非常重要,除了忠實的將外包廠商的回饋傳達給內部技術人員外,也必須適度的建立內部技術人員與外包廠商技術人員的直接溝通管道,以擴大溝通層面和深度,如此也能提升外包成品的品質。
  • 驗收
    針對軟體外包,驗收過程除了基本的測試外,也必須強調驗收的時效性。若驗收時程過長,除了會打擊雙方廠商的士氣、拖延開發進度外,更會提高外包破局的機率,導致原本的外包案件變成半外包、半自行開發的狀態(變成倚賴內部技術人員直接檢查外包成品以進行修改),已致完全違反了初衷。唯有快速的反應驗收結果,要求外包廠商期限內提出修正版本才是上策。

上述關鍵因素只要有一項出現瑕疵,即會給軟體外包結果帶來重大傷害,小則打擊員工士氣、重則拖慢開發時程,影響企業獲利,不可不慎。希望此篇短文能為執行軟體外包的企業、公司帶來些許的參考價值,進而避免犯下錯誤。

DateTime.ToString Issue in .NET

When we are programming in .NET platform, one of common task is using DateTime.ToString method to output a time string in certain pattern.

We all think that the output string will follows the pattern which we asign to the parameter in ToString method. However, sometime you will get a strange result. You may consider the code and result below:

DateTime.Now.ToString("yyyyMMdd"); -> 20070607
DateTime.Now.ToString("yyyy-MM-dd"); -> 2007-06-07
DateTime.Now.ToString("yyyy/MM/dd"); -> 2007-06-07
DateTime.Now.ToString("yyyy年MM月dd日"); -> 2007年06月07日
DateTime.Now.ToString("yyyy~MM~dd"); -> 2007~06~07


watch out the third line and you may think that should not be happened. But I really met this kind of problem in Windows Server 2000 today.

Why it happened? Because it is caused by the setting of short time format in your OS. In Windows Server 2000, You could go to the "regional options" in control pannel then switch to "Date" tab. In the dialog window, there is a "Date separator" option. You need to modify it from any other to "/". (Check the image below.)



If you want to make sure that your program will get the right string format in any setting condition to any windows computer, then The only way is using an overwrite versoin of DateTime.ToString method and pass the "System.Globalization.DateTimeFormatInfo.InvariantInfo" as second parameter. So the ultimate solution which don't need to configure your OS setting to the third line above is:
DateTime.NowToString( "yyyy/M/d", System.Globalization.DateTimeFormatInfo.InvariantInfo );

then the result will be exactly 2007/06/07.

Very funny, right? Hope Microsoft will fix this problem in the future.
[re-post this article due to database crash.]

MCMS 系統 Save 及 Publish Posting 程式開發指引

在 MCMS 系統中,如果要以程式的方式來建立或修改 Posting,必須透過一定的流程。然而,因為相關的資訊及文件十分缺乏,導致我自行摸索了許久,以下便將心得與大家分享。

在程式進行 MCMS 的 Posting 更新時,首先必須切換至 MCMS 的編輯模式,此時便需要進行身份驗證的動作。在開發 ASP.NET 程式時,我們通常會以目前 access 頁面的使用者帳戶直接進行驗證,以確定該名使用者具有足夠的權限進行後續操作,要達到上述的目的,請參考以下的程式碼片段:

WindowsIdetity wi = HttpContext.Current.User.Identity as WindowsIdentity;
CmsApplicationContext cac = new CmsApplicationContext();
cac.AuthenticateUsingUserHandle( wi.Token, PublishingMode.Update );


進行身份驗證並切換至編輯模式後,接下來,便是取得欲更新的 Posting 標的,在 MCMS 系統當中,針對每一 Posting 均會維護數種版本,包括歷次發佈的歷史版本等。因為我們是要針對某篇 Posting 做更新動作,所以應當取出該篇 Posting 的最新版本來更新,若先前切換成編輯模式成功,則取出某篇 Posting 的最新版本便輕而易舉,直接呼叫 CmsApplicationContext 物件的 Searches 類別方法即可:

Posting p = cac.Searches.GetByPath( "Posting Path" ) as Posting;

有些時候,你可能會想在同一個操作頁面秀出目前的發佈版本及目前編輯的最新版本,以作為交互比對之用,此時便需要取出上一次發佈的 Posting 版本,該怎麼做呢?請參考以下程式碼:

Posting p = cac.Searches.GetByPath( "Posting Path" ) as Posting;
PostingCollection pc = p.Revisions( true );
p = pc[pc.Count - 1];


一開始,同樣是呼叫 CmsApplicationContext 物件的 Searches 類別方法取出 Posting 物件,就跟取出最新版本的例子一樣,但是接下來,則呼叫 Posting 的 Revisoins 方法取出 PostingCollection 陣列物件。我們在此是傳入 true 當參數,用以取得該 Posting 以前所有發佈過的版本。最後,從回傳的 Posting 陣列中取出最後一個 Posting 物件,即是最近一次的發佈版本了!

再來,便是設定更新值的處理。在 MCMS 的 Posting 中,真正儲存資料的地方是 Placeholder(一篇 Posting 通常會包含數個 Placeholder 以存放資料,端看產生此篇 Posting 的 Template 如何設計而定),以下便以 XmlPlaceholder 為例,示範如何撰寫更新動作的程式碼:

XmlPlaceholder xp = p.Placeholder["Placeholder name"] as XmlPlaceholder;
xp.XmlAsString = "Your new xml content";


在更新動作完成後,有兩種選擇,一是將更新儲存但保持未發佈狀態,二是直接將更新後的 Posting 發佈。前者只會建立一新版本的 Posting 並保存更新狀態,真正供一般使用者瀏覽的 Posting 版本仍是上次發佈的版本。先示範儲存的寫法:

cac.CommitAll();

非常簡單,僅需呼叫 CmsApplicationContext 物件的 CommitAll 方法。再來示範直接發佈的寫法:

if( p.CanSubmit ) { p.Submit(); }
if( p.CanApprove ) { p.Approve(); }
cac.CommitAll();


欲直接發佈 Posting 時,要先呼叫該 Posting 物件的 Submit 方法,再呼叫 Approve 方法,最後跟儲存時的寫法一樣,呼叫 CmsApplicationContext 物件的 CommitAll 方法作為結束。上述的程式碼片段,使用了 CanSubmit 和 CanApprove 兩個 Posting 屬性做確認動作,可防止因登入使用者權限不足而導致的例外錯誤擲出。

以上,便是在撰寫 MCMS Posting 更新程式時的主要關鍵步驟,供大家參考。

[re-post this article due to database crash.]

Use .NET program to renew GSA crawling header

Now, I gonna guide you how to submitting form datas to GSA by using HTTP header as example.

In my previous article [Using .NET HttpWebRequest to pass through GSA login form], I talked about how to use .NET HttpWebRequest object to pass through GSA(Google Search Appliance) authentication framework. Now, I gonna guide you how to submitting form datas to GSA. (I will use HTTP header setting page under GSA admin console as example.)

Imagine that you have a system which only allows authenticated user and you want to configure GSA to crawl into it. That system does not integrate with Microsoft Active Directory server and has its own authentication framework, so we plan to get a cookie for GSA from that system. However, it is a ASP.NET web application running .NET 2.0 run time and does not provide indefinite duration cookie, so any cookie that we assign to GSA will be expired after a period of time. Although we could manually renew GSA cookie but it is much preferable that we could develop a program doing it for us. So here is the solution.

First, you need to follow the procedure that I introduced in [Using .NET HttpWebRequest to pass through GSA login form] to login GSA admin console page. Then, because we will submitting our data to HTTP Header setting page through HTTP POST, so we need to build the data string first. There has two input columns on HTTP Header page of Google Admin Console, one is "user agent name" and the other is "HTTP header". Please reference the full string from below:

string strContent = "actionType=httpHeaders&userAgent=[user agent name]&addHTTPHdrs=" + Server.UrlEncode("[HTTP header content]") + "&httpHeadersSave=+%E6%9B%B4%E6%96%B0%E6%A8%99%E9%A1%8C%E8%A8%AD%E5%AE%9A";

Please pay attention on a special key value of "HttpHeadersSave". You need to add it into full string or form submit won't be successful. Also, the HTTP header may contain special character so we use UrlEncode method of Server object to encode it. Once we have the full data string, we need to tranfer it to byte array:

ASCIIEncoding asciienc = new ASCIIEncoding();
byte[] byteContent = asciienc.GetBytes( strContent );


Finally, we will use HttpWebRequest object again to send out the request. The objects "req", "cc" and "s" below are HttpWebRequest, CookieContainer and Stream classes we created when logon GSA admin console. (Please reference the article [Using .NET HttpWebRequest to pass through GSA login form] for the details.)

req = HttpWebRequest.Create("http://[GSA URL]/EnterpriseController") as HttpWebRequest;
req.CookieContainer = cc;
req.Referer = "http://[GSA URL]/EnterpriseController?actionType=httpHeaders";
req.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)";
req.ContentType = "application/x-www-form-urlencoded";
req.Method = "POST";
req.ContentLength = byteContent.Length;

s = req.GetRequestStream();
s.Write(byteContent, 0, byteContent.Length);
s.Flush();
s.Close();


That's all! You could based on the codes above to develop programs submitting data to all kind of GSA admin console pages.

[re-post this article due to database crash.]

Use .NET HttpWebRequest class to pass through GSA login form

Now let me show you how to use HttpWebRequest to pass through GSA login form. After that, you can extract the web page content by yourself.

The GSA (Google Search Appliance) login form has a special authentication framework to pervent the hacking. It is based on assigning several cookies to client browser during HTTP connecting session. Now let me show you how to use HttpWebRequest to pass through GSA login form. After that, you can extract the web page content by yourself.

First, you need to maintain a CookieContainer object during the whole process:
CookieContainer cc = new CookieContainer();

Then, we need to create a byte array to store the login data, please replace [User Name] and [password] in string loginData below to your case in GSA:
string loginData = "actionType=authenticateUser&login=Login&userName=[User Name]&password=[Password]";
ASCIIEncoding asciiEnc = new ASCIIEncoding();
byte[] byteArray = asciiEnc.GetBytes(loginData);

process 3 times http request to pass through GSA login form, please replace [GSA URL] to your case:
// first time
HttpWebRequest req = HttpWebRequest.Create("http://[GSA URL]/") as HttpWebRequest;
req.CookieContainer = cc;
req.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)";
HttpWebResponse res = req.GetResponse() as HttpWebResponse;

// second time
req = HttpWebRequest.Create("http://[GSA URL]/EnterpriseController") as HttpWebRequest;
req.CookieContainer = cc;
req.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)";
res = req.GetResponse() as HttpWebResponse;

// third time
req = HttpWebRequest.Create("http://[GSA URL]/EnterpriseController") as HttpWebRequest;
req.CookieContainer = cc;
req.Referer = http://[GSA URL]/EnterpriseController;
req.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)";
req.ContentType = "application/x-www-form-urlencoded";
req.Method = "POST";
req.ContentLength = byteArray.Length;
Stream s = req.GetRequestStream();
s.Write(byteArray, 0, byteArray.Length);
s.Close();
res = req.GetResponse() as HttpWebResponse;

OK, now we can pass parameters to get any web page content, please replace [Parameters] from code snippet below to your customized one:
req = HttpWebRequest.Create(http://[GSA URL]/EnterpriseController/?actionType=[Parameters]") as HttpWebRequest;
req.CookieContainer = cc;
req.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)";
res = req.GetResponse() as HttpWebResponse;
s = res.GetResponseStream();
StreamReader sr = new StreamReader(s);
string result = sr.ReadToEnd();
sr.Close();
s.Close();

The result string is the final web page content.

[re-post this article due to database crash.]